-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[2.1] Extracting translations support. #1259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1dd7638
4fb6cf1
dbfa4e7
2a934f9
0881804
9213559
366f097
e766ed1
73a1bd0
d59ac1d
0897d7d
e62f5d2
903ee39
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 |
---|---|---|
@@ -0,0 +1,210 @@ | ||
<?php | ||
|
||
namespace Symfony\Bundle\FrameworkBundle\Command; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Finder\Finder; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputOption; | ||
|
||
class UpdateTransCommand extends Command { | ||
|
||
/** | ||
* Deafult domain for found trans blocks/filters | ||
* | ||
* @var string | ||
*/ | ||
private $defaultDomain = 'messages'; | ||
|
||
/** | ||
* Prefix for newly found message ids | ||
* | ||
* @var string | ||
*/ | ||
protected $prefix; | ||
|
||
/** | ||
* Compiled catalogue of messages | ||
* @var \Symfony\Component\Translation\MessageCatalogue | ||
*/ | ||
protected $messages; | ||
|
||
/** | ||
* @see Command | ||
*/ | ||
protected function configure() | ||
{ | ||
$this | ||
->setName('trans:update') | ||
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 would use |
||
->setDescription('Update the translation file') | ||
->setDefinition(array( | ||
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), | ||
new InputArgument('bundle', InputArgument::REQUIRED, 'The bundle where to load the messages'), | ||
new InputOption( | ||
'prefix', null, InputOption::VALUE_OPTIONAL, | ||
'Override the default prefix', '__' | ||
), | ||
new InputOption( | ||
'output-format', null, InputOption::VALUE_OPTIONAL, | ||
'Override the default output format (yml, xliff, php or pot)', 'yml' | ||
), | ||
new InputOption( | ||
'source-lang', null, InputOption::VALUE_OPTIONAL, | ||
'Set the source language attribute in xliff files', 'en' | ||
), | ||
new InputOption( | ||
'dump-messages', null, InputOption::VALUE_NONE, | ||
'Should the messages be dumped in the console' | ||
), | ||
new InputOption( | ||
'force', null, InputOption::VALUE_NONE, | ||
'Should the update be done' | ||
) | ||
)); | ||
} | ||
|
||
/** | ||
* @see Command | ||
*/ | ||
protected function execute(InputInterface $input, OutputInterface $output) | ||
{ | ||
$twig = $this->container->get('twig'); | ||
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 is wrong. The command should be usable even when Twig is disabled. |
||
$this->prefix = $input->getOption('prefix'); | ||
|
||
if ($input->getOption('force') !== true && $input->getOption('dump-messages') !== true) { | ||
$output->writeln('You must choose one of --force or --dump-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. you should add a |
||
} else { | ||
|
||
// get bundle directory | ||
$foundBundle = $this->getApplication()->getKernel()->getBundle($input->getArgument('bundle')); | ||
$bundleTransPath = $foundBundle->getPath() . '/Resources/translations'; | ||
$output->writeln(sprintf('Generating "<info>%s</info>" translation files for "<info>%s</info>"', $input->getArgument('locale'), $foundBundle->getName())); | ||
|
||
$output->writeln('Parsing files.'); | ||
|
||
// load any messages from templates | ||
$this->messages = new \Symfony\Component\Translation\MessageCatalogue($input->getArgument('locale')); | ||
$finder = new Finder(); | ||
$files = $finder->files()->name('*.html.twig')->in($foundBundle->getPath() . '/Resources/views/'); | ||
foreach ($files as $file) { | ||
$output->writeln(sprintf(' > parsing template <comment>%s</comment>', $file->getPathname())); | ||
$tree = $twig->parse($twig->tokenize(file_get_contents($file->getPathname()))); | ||
$this->_crawlNode($tree); | ||
} | ||
|
||
foreach($this->container->findTaggedServiceIds('translation.loader') as $id => $attributes) { | ||
// load any existing translation files | ||
$finder = new Finder(); | ||
$files = $finder->files()->name('*.' . $input->getArgument('locale') . $attributes[0]['alias'])->in($bundleTransPath); | ||
foreach ($files as $file) { | ||
$output->writeln(sprintf(' > parsing translation <comment>%s</comment>', $file->getPathname())); | ||
$domain = substr($file->getFileName(), 0, strrpos($file->getFileName(), $input->getArgument('locale') . $attributes[0]['alias']) - 1); | ||
$loader = $this->container->get($id); | ||
$this->messages->addCatalogue($loader->load($file->getPathname(), $input->getArgument('locale'), $domain)); | ||
} | ||
} | ||
|
||
// show compiled list of messages | ||
if($input->getOption('dump-messages') === true){ | ||
foreach ($this->messages->getDomains() as $domain) { | ||
$output->writeln(sprintf("\nDisplaying messages for domain <info>%s</info>:\n", $domain)); | ||
$output->writeln(\Symfony\Component\Yaml\Yaml::dump($this->messages->all($domain),10)); | ||
} | ||
} | ||
|
||
// save the files | ||
if($input->getOption('force') === true) { | ||
$output->writeln("\nWriting files.\n"); | ||
$path = $foundBundle->getPath() . '/Resources/translations/'; | ||
foreach($this->container->findTaggedServiceIds('translation.formatter') as $id => $attributes) { | ||
if ($input->getOption('output-format') == $attributes[0]['alias']) { | ||
$formatter = $this->container->get($id); | ||
break; | ||
} | ||
} | ||
foreach ($this->messages->getDomains() as $domain) { | ||
$file = $domain . '.' . $input->getArgument('locale') . '.' . $input->getOption('output-format'); | ||
if (file_exists($path . $file)) { | ||
copy($path . $file, $path . '~' . $file . '.bak'); | ||
} | ||
$output->writeln(sprintf(' > generating <comment>%s</comment>', $path . $file)); | ||
file_put_contents($path . $file, $formatter->format($this->messages->all($domain))); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Recursive function that extract trans message from a twig tree | ||
* | ||
* @param \Twig_Node The twig tree root | ||
*/ | ||
private function _crawlNode(\Twig_Node $node) | ||
{ | ||
if ($node instanceof \Symfony\Bridge\Twig\Node\TransNode && !$node->getNode('body') instanceof \Twig_Node_Expression_GetAttr) { | ||
// trans block | ||
$domain = $node->getNode('domain')->getAttribute('value'); | ||
$message = $node->getNode('body')->getAttribute('data'); | ||
$this->messages->set($message, $this->prefix.$message, $domain); | ||
} else if ($node instanceof \Twig_Node_Print) { | ||
// trans filter (be carefull of how you chain your filters) | ||
$message = $this->_extractMessage($node->getNode('expr')); | ||
$domain = $this->_extractDomain($node->getNode('expr')); | ||
if($message !== null && $domain!== null) { | ||
$this->messages->set($message, $this->prefix.$message, $domain); | ||
} | ||
} else { | ||
// continue crawling | ||
foreach ($node as $child) { | ||
if ($child != null) { | ||
$this->_crawlNode($child); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Extract a message from a \Twig_Node_Print | ||
* Return null if not a constant message | ||
* | ||
* @param \Twig_Node $node | ||
*/ | ||
private function _extractMessage(\Twig_Node $node) | ||
{ | ||
if($node->hasNode('node')) { | ||
return $this->_extractMessage($node->getNode ('node')); | ||
} | ||
if($node instanceof \Twig_Node_Expression_Constant) { | ||
return $node->getAttribute('value'); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Extract a domain from a \Twig_Node_Print | ||
* Return null if no trans filter | ||
* | ||
* @param \Twig_Node $node | ||
*/ | ||
private function _extractDomain(\Twig_Node $node) | ||
{ | ||
// must be a filter node | ||
if(!$node instanceof \Twig_Node_Expression_Filter) { | ||
return null; | ||
} | ||
// is a trans filter | ||
if($node->getNode('filter')->getAttribute('value') == 'trans') { | ||
if($node->getNode('arguments')->hasNode(1)) { | ||
return $node->getNode('arguments')->getNode(1)->getAttribute('value'); | ||
} else { | ||
return $this->defaultDomain; | ||
} | ||
} | ||
|
||
return $this->_extractDomain($node->getNode('node')); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Translation\Formatter; | ||
|
||
/** | ||
* Interface for formatters | ||
*/ | ||
interface FormatterInterface | ||
{ | ||
/** | ||
* Generates a string representation of the message format. | ||
* | ||
* @param $messages array | ||
* @return string | ||
*/ | ||
public function format(array $messages); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Translation\Formatter; | ||
|
||
class PhpFormatter implements FormatterInterface | ||
{ | ||
public function format(array $messages) | ||
{ | ||
$output = "<?php\nreturn ".var_export($messages, true).";"; | ||
|
||
return $output; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Translation\Formatter; | ||
|
||
class PotFormatter implements FormatterInterface | ||
{ | ||
public function format(array $messages) | ||
{ | ||
$output[] = 'msgid ""'; | ||
$output[] = 'msgstr ""'; | ||
$output[] = '"Project-Id-Version: PACKAGE VERSION\n"'; | ||
$output[] = '"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"'; | ||
$output[] = '"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"'; | ||
$output[] = '"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"'; | ||
$output[] = '"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"'; | ||
$output[] = '"MIME-Version: 1.0\n"'; | ||
$output[] = '"Content-Type: text/plain; charset=UTF-8\n"'; | ||
$output[] = '"Content-Transfer-Encoding: 8bit\n"'; | ||
$output[] = '"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"'; | ||
$output[] = ''; | ||
|
||
foreach ($data as $source => $target) { | ||
$source = $this->clean($source); | ||
$target = $this->clean($target); | ||
|
||
$output[] = "msgid \"{$source}\""; | ||
$output[] = "msgstr \"{$target}\""; | ||
$output[] = ''; | ||
} | ||
|
||
return implode("\n", $output) . "\n"; | ||
} | ||
|
||
protected function clean($message) | ||
{ | ||
$message = strtr($message, array("\\'" => "'", "\\\\" => "\\", "\r\n" => "\n")); | ||
$message = addcslashes($message, "\0..\37\\\""); | ||
return $message; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Translation\Formatter; | ||
|
||
class XliffFormatter implements FormatterInterface | ||
{ | ||
private $source; | ||
|
||
/** | ||
* @param string $source source-language attribute for the <file> element | ||
*/ | ||
public function __construct($source = 'en') | ||
{ | ||
$this->source = $source; | ||
} | ||
|
||
public function format(array $messages) | ||
{ | ||
$dom = new \DOMDocument('1.0', 'utf-8'); | ||
$dom->formatOutput = true; | ||
$xliff = $dom->appendChild($dom->createElement('xliff')); | ||
$xliff->setAttribute('version', '1.2'); | ||
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); | ||
$xliffFile = $xliff->appendChild($dom->createElement('file')); | ||
$xliffFile->setAttribute('source-language', $this->source); | ||
$xliffFile->setAttribute('datatype', 'plaintext'); | ||
$xliffFile->setAttribute('original', 'file.ext'); | ||
$xliffBody = $xliffFile->appendChild($dom->createElement('body')); | ||
$id = 1; | ||
foreach ($messages as $source => $target) { | ||
$trans = $dom->createElement('trans-unit'); | ||
$trans->setAttribute('id', $id); | ||
$s = $trans->appendChild($dom->createElement('source')); | ||
$s->appendChild($dom->createTextNode($source)); | ||
$t = $trans->appendChild($dom->createElement('target')); | ||
$t->appendChild($dom->createTextNode($target)); | ||
$xliffBody->appendChild($trans); | ||
$id++; | ||
} | ||
|
||
return $dom->saveXML(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Translation\Formatter; | ||
|
||
use \Symfony\Component\Yaml\Yaml; | ||
|
||
class YamlFormatter implements FormatterInterface | ||
{ | ||
public function format(array $messages) | ||
{ | ||
return Yaml::dump($messages); | ||
} | ||
} |
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.
The curly braces should be on the next line.