diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
index 9553d4d60d7ce..4369a5549f367 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
@@ -113,6 +113,13 @@
+
+
+
+
+
+
+
diff --git a/src/Symfony/Component/Translation/Command/IntlConvertCommand.php b/src/Symfony/Component/Translation/Command/IntlConvertCommand.php
new file mode 100644
index 0000000000000..745881c634273
--- /dev/null
+++ b/src/Symfony/Component/Translation/Command/IntlConvertCommand.php
@@ -0,0 +1,96 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Command;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\HttpKernel\KernelInterface;
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Reader\TranslationReaderInterface;
+use Symfony\Component\Translation\Util\IntlMessageConverter;
+use Symfony\Component\Translation\Writer\TranslationWriterInterface;
+
+/**
+ * Convert to Intl styled message format.
+ *
+ * @author Tobias Nyholm
+ */
+class IntlConvertCommand extends Command
+{
+ protected static $defaultName = 'translation:convert-to-intl-messages';
+
+ private $writer;
+ private $reader;
+
+ public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader)
+ {
+ parent::__construct();
+
+ $this->writer = $writer;
+ $this->reader = $reader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setDescription('Convert from Symfony 3 plural format to ICU message format.')
+ ->addArgument('locale', InputArgument::REQUIRED, 'The locale')
+ ->addArgument('path', null, 'A file or a directory')
+ ->addOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain')
+ ->addOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $io = new SymfonyStyle($input, $output);
+ $path = $input->getArgument('path');
+ $locale = $input->getArgument('locale');
+ $domain = $input->getOption('domain');
+ /** @var KernelInterface $kernel */
+ $kernel = $this->getApplication()->getKernel();
+
+ // Define Root Paths
+ $transPaths = $kernel->getProjectDir().\DIRECTORY_SEPARATOR.'translations';
+ if (null !== $path) {
+ $transPaths = $path;
+ }
+
+ // load any existing messages from the translation files
+ $currentCatalogue = new MessageCatalogue($locale);
+ if (!is_dir($transPaths)) {
+ throw new \LogicException('The "path" must be a directory.');
+ }
+ $this->reader->read($transPaths, $currentCatalogue);
+
+ $allMessages = $currentCatalogue->all($domain);
+ if (null !== $domain) {
+ $allMessages = array($domain => $allMessages);
+ }
+
+ $updated = array();
+ foreach ($allMessages as $messageDomain => $messages) {
+ foreach ($messages as $key => $message) {
+ $updated[$messageDomain][$key] = IntlMessageConverter::convert($message);
+ }
+ }
+
+ $this->writer->write(new MessageCatalogue($locale, $updated), $input->getOption('output-format'), array('path' => $transPaths));
+ }
+}
diff --git a/src/Symfony/Component/Translation/Tests/Util/IntlMessageConverterTest.php b/src/Symfony/Component/Translation/Tests/Util/IntlMessageConverterTest.php
new file mode 100644
index 0000000000000..7aef95df822b8
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/Util/IntlMessageConverterTest.php
@@ -0,0 +1,96 @@
+assertEquals($output, $result);
+ }
+
+ public function testConvertWithCustomDelimiter()
+ {
+ $result = IntlMessageConverter::convert('Foo #var# bar', '#');
+ $this->assertEquals('Foo {var} bar', $result);
+
+ $result = IntlMessageConverter::convert('{0} Foo #var# bar | {1} Bar #var# foo', '#');
+ $this->assertEquals(
+ <<expectException(\LogicException::class);
+ IntlMessageConverter::convert(']-Inf, -2[ Negative|]1,Inf[ Positive');
+ }
+
+ public function getTestData()
+ {
+ yield array('|', '|');
+ yield array(
+ '{0} There are no apples|{1} There is one apple|]1,Inf[ There %name% are %count% apples',
+ <<
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Util;
+
+/**
+ * Convert from Symfony 3's plural syntax to Intl message format.
+ * {@link https://messageformat.github.io/messageformat/page-guide}.
+ *
+ * @author Tobias Nyholm
+ */
+class IntlMessageConverter
+{
+ public static function convert(string $message, string $variableDelimiter = '%'): string
+ {
+ $array = self::getMessageArray($message);
+ if (empty($array)) {
+ return $message;
+ }
+
+ if (1 === \count($array) && isset($array[0])) {
+ return self::replaceVariables($message, $variableDelimiter);
+ }
+
+ $icu = self::buildIcuString($array, $variableDelimiter);
+
+ return $icu;
+ }
+
+ /**
+ * Get an ICU like array.
+ */
+ private static function getMessageArray(string $message): array
+ {
+ if (preg_match('/^\|++$/', $message)) {
+ // If the message only contains pipes ("|||")
+ return array();
+ } elseif (preg_match_all('/(?:\|\||[^\|])++/', $message, $matches)) {
+ $parts = $matches[0];
+ } else {
+ throw new \LogicException(sprintf('Input string "%s" is not supported.', $message));
+ }
+
+ $intervalRegexp = <<<'EOF'
+/^(?P
+ ({\s*
+ (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
+ \s*})
+
+ |
+
+ (?P[\[\]])
+ \s*
+ (?P-Inf|\-?\d+(\.\d+)?)
+ \s*,\s*
+ (?P\+?Inf|\-?\d+(\.\d+)?)
+ \s*
+ (?P[\[\]])
+)\s*(?P.*?)$/xs
+EOF;
+
+ $standardRules = array();
+ foreach ($parts as $part) {
+ $part = trim(str_replace('||', '|', $part));
+
+ // try to match an explicit rule, then fallback to the standard ones
+ if (preg_match($intervalRegexp, $part, $matches)) {
+ if ($matches[2]) {
+ foreach (explode(',', $matches[3]) as $n) {
+ $standardRules['='.$n] = $matches['message'];
+ }
+ } else {
+ $leftNumber = '-Inf' === $matches['left'] ? -INF : (float) $matches['left'];
+ $rightNumber = \is_numeric($matches['right']) ? (float) $matches['right'] : INF;
+
+ $leftNumber = ('[' === $matches['left_delimiter'] ? $leftNumber : 1 + $leftNumber);
+ $rightNumber = (']' === $matches['right_delimiter'] ? 1 + $rightNumber : $rightNumber);
+
+ if ($leftNumber !== -INF && INF !== $rightNumber) {
+ for ($i = $leftNumber; $i < $rightNumber; ++$i) {
+ $standardRules['='.$i] = $matches['message'];
+ }
+ } else {
+ // $rightNumber is INF or $leftNumber is -INF
+ if (isset($standardRules['other'])) {
+ throw new \LogicException(sprintf('%s does not support converting messages with both "-Inf" and "Inf". Message: "%s"', __CLASS__, $message));
+ }
+ $standardRules['other'] = $matches['message'];
+ }
+ }
+ } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) {
+ $standardRules[] = $matches[1];
+ } else {
+ $standardRules[] = $part;
+ }
+ }
+
+ return $standardRules;
+ }
+
+ private static function buildIcuString(array $data, string $variableDelimiter): string
+ {
+ $icu = "{ COUNT, plural,\n";
+ foreach ($data as $key => $message) {
+ $message = strtr($message, array('%count%' => '#'));
+ $message = self::replaceVariables($message, $variableDelimiter);
+ $icu .= sprintf(" %s {%s}\n", $key, $message);
+ }
+ $icu .= '}';
+
+ return $icu;
+ }
+
+ private static function replaceVariables(string $message, string $variableDelimiter): string
+ {
+ $regex = sprintf('|%s(.*?)%s|s', $variableDelimiter, $variableDelimiter);
+
+ return preg_replace($regex, '{$1}', $message);
+ }
+}