8000 -- add command that extracts translation messages from templates · symfony/symfony@ef322f6 · GitHub
[go: up one dir, main page]

Skip to content

Commit ef322f6

Browse files
author
Michel Salib
committed
-- add command that extracts translation messages from templates
-- add missing files -- tweak translation command files -- dumpers are now responsive for writting the files -- moved the twig extractor the bridge -- clear temp files after unit tests -- check the presence of dumper in translation writer -- General cleaning of the code -- clean phpDoc -- fix PHPDoc -- fixing class name in configuration -- add unit tests for extractors (php and twig) -- moved test to correct location -- polish the code -- polish the code
1 parent 49e1eee commit ef322f6

File tree

30 files changed

+1002
-47
lines changed

30 files changed

+1002
-47
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bridge\Twig\Translation;
13+
14+
use Symfony\Component\Finder\Finder;
15+
use Symfony\Component\Translation\Extractor\ExtractorInterface;
16+
use Symfony\Component\Translation\MessageCatalogue;
17+
use Symfony\Bridge\Twig\Node\TransNode;
18+
19+
/**
20+
* TwigExtractor extracts translation messages from a twig template.
21+
*
22+
* @author Michel Salib <michelsalib@hotmail.com>
23+
*/
24+
class TwigExtractor implements ExtractorInterface
25+
{
26+
/**
27+
* Default domain for found messages.
28+
*
29+
* @var string
30+
*/
31+
private $defaultDomain = '';
32+
33+
/**
34+
* Prefix for found message.
35+
*
36+
* @var string
37+
*/
38+
private $prefix = '';
39+
40+
/**
41+
* The twig environment.
42+
* @var \Twig_Environment
43+
*/
44+
private $twig;
45+
46+
public function __construct(\Twig_Environment $twig)
47+
{
48+
$this->twig = $twig;
49+
}
50+
51+
/**
52+
* {@inheritDoc}
53+
*/
54+
public function extract($directory, MessageCatalogue $catalogue)
55+
{
56+
// load any existing translation files
57+
$finder = new Finder();
58+
$files = $finder->files()->name('*.twig')->in($directory);
59+
foreach ($files as $file) {
60+
$tree = $this->twig->parse($this->twig->tokenize(file_get_contents($file->getPathname())));
61+
$this->crawlNode($tree, $catalogue);
62+
}
63+
}
64+
65+
/**
66+
* {@inheritDoc}
67+
*/
68+
public function setPrefix($prefix)
69+
{
70+
$this->prefix = $prefix;
71+
}
72+
73+
/**
74+
* Extracts trans message from a twig tree.
75+
*
76+
* @param \Twig_Node $node The twig tree root
77+
* @param MessageCatalogue $catalogue The catalogue
78+
*/
79+
private function crawlNode(\Twig_Node $node, MessageCatalogue $catalogue)
80+
{
81+
if ($node instanceof TransNode && !$node->getNode('body') instanceof \Twig_Node_Expression_GetAttr) {
82+
// trans block
83+
$message = $node->getNode('body')->getAttribute('data');
84+
$domain = $node->getNode('domain')->getAttribute('value');
85+
$catalogue->set($message, $this->prefix.$message, $domain);
86+
} elseif ($node instanceof \Twig_Node_Print) {
87+
// trans filter (be carefull of how you chain your filters)
88+
$message = $this->extractMessage($node->getNode('expr'));
89+
$domain = $this->extractDomain($node->getNode('expr'));
90+
if ($message !== null && $domain !== null) {
91+
$catalogue->set($message, $this->prefix.$message, $domain);
92+
}
93+
} else {
94+
// continue crawling
95+
foreach ($node as $child) {
96+
if ($child != null) {
97+
$this->crawlNode($child, $catalogue);
98+
}
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Extracts a message from a \Twig_Node_Print.
105+
* Return null if not a constant message.
106+
*
107+
* @param \Twig_Node $node
108+
* @return The message (or null)
109+
*/
110+
private function extractMessage(\Twig_Node $node)
111+
{
112+
if ($node->hasNode('node')) {
113+
return $this->extractMessage($node->getNode('node'));
114+
}
115+
if ($node instanceof \Twig_Node_Expression_Constant) {
116+
return $node->getAttribute('value');
117+
}
118+
119+
return null;
120+
}
121+
122+
/**
123+
* Extracts a domain from a \Twig_Node_Print.
124+
* Return null if no trans filter.
125+
*
126+
* @param \Twig_Node $node
127+
* @return The domain (or null)
128+
*/
129+
private function extractDomain(\Twig_Node $node)
130+
{
131+
// must be a filter node
132+
if (!$node instanceof \Twig_Node_Expression_Filter) {
133+
return null;
134+
}
135+
// is a trans filter
136+
if ($node->getNode('filter')->getAttribute('value') === 'trans') {
137+
if ($node->getNode('arguments')->hasNode(1)) {
138+
return $node->getNode('arguments')->getNode(1)->getAttribute('value');
139+
}
140+
141+
return $this->defaultDomain;
142+
}
143+
144+
return $this->extractDomain($node->getNode('node'));
145+
}
146+
}
147+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Finder\Finder;
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Translation\MessageCatalogue;
21+
use Symfony\Component\Yaml\Yaml;
22+
23+
/**
24+
* A command that parse templates to extract translation messages and add them into the translation files.
25+
*
26+
* @author Michel Salib <michelsalib@hotmail.com>
27+
*/
28+
class TranslationUpdateCommand extends ContainerAwareCommand
29+
{
30+
/**
31+
* Compiled catalogue of messages.
32+
* @var MessageCatalogue
33+
*/
34+
protected $catalogue;
35+
36+
/**
37+
* {@inheritDoc}
38+
*/
39+
protected function configure()
40+
{
41+
$this
42+
->setName('translation:update')
43+
->setDescription('Update the translation file')
44+
->setDefinition(array(
45+
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
46+
new InputArgument('bundle', InputArgument::REQUIRED, 'The bundle where to load the messages'),
47+
new InputOption(
48+
'prefix', null, InputOption::VALUE_OPTIONAL,
49+
'Override the default prefix', '__'
50+
),
51+
new InputOption(
52+
'output-format', null, InputOption::VALUE_OPTIONAL,
53+
'Override the default output format', 'yml'
54+
),
55+
new InputOption(
56+
'dump-messages', null, InputOption::VALUE_NONE,
57+
'Should the messages be dumped in the console'
58+
),
59+
new InputOption(
60+
'force', null, InputOption::VALUE_NONE,
61+
'Should the update be done'
62+
)
63+
))
64+
->setHelp(<<<EOF
65+
The <info>translation:update</info> command extract translation strings from templates
66+
of a given bundle. It can display them or merge the new ones into the translation files.
67+
When new translation strings are found it can automatically add a prefix to the translation
68+
message.
69+
70+
<info>php app/console translation:update --dump-messages en AcmeBundle</info>
71+
<info>php app/console translation:update --force --prefix="new_" fr AcmeBundle</info>
72+
EOF
73+
);
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
protected function execute(InputInterface $input, OutputInterface $output)
80+
{
81+
// check presence of force or dump-message
82+
if ($input->getOption('force') !== true && $input->getOption('dump-messages') !== true) {
83+
$output->writeln('<info>You must choose one of --force or --dump-messages</info>');
84+
return;
85+
}
86+
87+
// check format
88+
$writer = $this->getContainer()->get('translation.writer');
89+
$supportedFormats = $writer->getFormats();
90+
if (!in_array($input->getOption('output-format'), $supportedFormats)) {
91+
$output->writeln('<error>Wrong output format</error>');
92+
$output->writeln('Supported formats are '.implode(', ', $supportedFormats).'.');
93+
return;
94+
}
95+
96+
// get bundle directory
97+
$foundBundle = $this->getApplication()->getKernel()->getBundle($input->getArgument('bundle'));
98+
$bundleTransPath = $foundBundle->getPath().'/Resources/translations';
99+
$output->writeln(sprintf('Generating "<info>%s</info>" translation files for "<info>%s</info>"', $input->getArgument('locale'), $foundBundle->getName()));
100+
101+
// create catalogue
102+
$catalogue = new MessageCatalogue($input->getArgument('locale'));
103+
104+
// load any messages from templates
105+
$output->writeln('Parsing templates');
106+
$extractor = $this->getContainer()->get('translation.extractor');
107+
$extractor->setPrefix($input->getOption('prefix'));
108+
$extractor->extractMessages($foundBundle->getPath().'/Resources/views/', $catalogue);
109+
110+
// load any existing messages from the translation files
111+
$output->writeln('Loading translation files');
112+
$loader = $this->getContainer()->get('translation.loader');
113+
$loader->loadMessages($bundleTransPath, $catalogue);
114+
115+
// show compiled list of messages
116+
if($input->getOption('dump-messages') === true){
117+
foreach ($catalogue->getDomains() as $domain) {
118+
$output->writeln(sprintf("\nDisplaying messages for domain <info>%s</info>:\n", $domain));
119+
$output->writeln(Yaml::dump($catalogue->all($domain),10));
120+
}
121+
if($input->getOption('output-format') == 'xliff')
122+
$output->writeln('Xliff output version is <info>1.2/info>');
123+
}
124+
125+
// save the files
126+
if($input->getOption('force') === true) {
127+
$output->writeln('Writing files');
128+
$writer->writeTranslations($catalogue, $input->getOption('output-format'), array('path' => $bundleTransPath));
129+
}
130+
}
131+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Reference;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
18+
/**
19+
* Adds tagged translation.formatter services to translation writer
20+
*/
21+
class TranslationDumperPass implements CompilerPassInterface
22+
{
23+
public function process(ContainerBuilder $container)
24+
{
25+
if (!$container->hasDefinition('translation.writer')) {
26+
return;
27+
}
28+
29+
$definition = $container->getDefinition('translation.writer');
30+
31+
foreach ($container->findTaggedServiceIds('translation.dumper') as $id => $attributes) {
32+
$definition->addMethodCall('addDumper', array($attributes[0]['alias'], new Reference($id)));
33+
}
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Reference;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
18+
/**
19+
* Adds tagged translation.extractor services to translation extractor
20+
*/
21+
class TranslationExtractorPass implements CompilerPassInterface
22+
{
23+
public function process(ContainerBuilder $container)
24+
{
25+
if (!$container->hasDefinition('translation.extractor')) {
26+
return;
27+
}
28+
29+
$definition = $container->getDefinition('translation.extractor');
30+
31+
foreach ($container->findTaggedServiceIds('translation.extractor') as $id => $attributes) {
32+
$definition->addMethodCall('addExtractor', array($attributes[0]['alias'], new Reference($id)));
33+
}
34+
}
35+
}

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

Lines changed: 9 additions & 0 deletions
17AE
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\Reference;
1415
use Symfony\Component\DependencyInjection\ContainerBuilder;
1516
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1617

@@ -26,6 +27,14 @@ public function process(ContainerBuilder $container)
2627
foreach ($container->findTaggedServiceIds('translation.loader') as $id => $attributes) {
2728
$loaders[$id] = $attributes[0]['alias'];
2829
}
30+
31+
if ($container->hasDefinition('translation.loader')) {
32+
$definition = $container->getDefinition('translation.loader');
33+
foreach ($loaders as $id => $format) {
34+
$definition->addMethodCall('addLoader', array($format, new Reference($id)));
35+
}
36+
}
37+
2938
$container->findDefinition('translator.default')->replaceArgument(2, $loaders);
3039
}
3140
}

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass;
2323
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
2424
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass;
25+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass;
26+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass;
2527
use Symfony\Component\DependencyInjection\ContainerBuilder;
2628
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
2729
use Symfony\Component\DependencyInjection\Scope;
@@ -57,6 +59,8 @@ public function build(ContainerBuilder $container)
5759
$container->addCompilerPass(new FormPass());
5860
$container->addCompilerPass(new TranslatorPass());
5961
$container->addCompilerPass(new AddCacheWarmerPass());
62+
$container->addCompilerPass(new TranslationExtractorPass());
63+
$container->addCompilerPass(new TranslationDumperPass());
6064

6165
if ($container->getParameter('kernel.debug')) {
6266
$container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING);

0 commit comments

Comments
 (0)
0