8000 [2.1] Extracting translations support. by xaav · Pull Request #1259 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[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

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Command/TransUpdateCommand.php
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 {

Copy link
Member

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.

/**
* 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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use translations: instead of trans:

->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');
Copy link
Member

Choose a reason for hiding this comment

The 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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add a return to get rid of the else enclosing the whole command

} 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
Expand Up @@ -11,6 +11,10 @@
<parameter key="translation.loader.php.class">Symfony\Component\Translation\Loader\PhpFileLoader</parameter>
<parameter key="translation.loader.yml.class">Symfony\Component\Translation\Loader\YamlFileLoader</parameter>
<parameter key="translation.loader.xliff.class">Symfony\Component\Translation\Loader\XliffFileLoader</parameter>
<parameter key="translation.formatter.php.class">Symfony\Component\Translation\Formatter\PhpFormatter</parameter>
<parameter key="translation.formatter.pot.class">Symfony\Component\Translation\Formatter\PotFormatter</parameter>
<parameter key="translation.formatter.xliff.class">Symfony\Component\Translation\Formatter\XliffFormatter</parameter>
<parameter key="translation.formatter.yml.class">Symfony\Component\Translation\Formatter\YamlFormatter</parameter>
</parameters>

<services>
Expand Down Expand Up @@ -42,5 +46,21 @@
<service id="translation.loader.xliff" class="%translation.loader.xliff.class%">
<tag name="translation.loader" alias="xliff" />
</service>

<service id="translation.formatter.php" class="%translation.formatter.php.class%">
<tag name="translation.formatter" alias="php" />
</service>

<service id="translation.formatter.pot" class="%translation.formatter.pot.class%">
<tag name="translation.formatter" alias="pot" />
</service>

<service id="tranlsation.formatter.xliff" class="%translation.formatter.xliff.class%">
<tag name="translation.formatter" alias="xliff" />
</service>

<service id="translation.formatter.yml" class="%translation.formatter.yml.class%">
<tag name="translation.formatter" alias="yml" />
</service>
</services>
</container>
18 changes: 18 additions & 0 deletions src/Symfony/Component/Translation/Formatter/FormatterInterface.php
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);

}
13 changes: 13 additions & 0 deletions src/Symfony/Component/Translation/Formatter/PhpFormatter.php
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;
}
}
40 changes: 40 additions & 0 deletions src/Symfony/Component/Translation/Formatter/PotFormatter.php
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;
}
}
44 changes: 44 additions & 0 deletions src/Symfony/Component/Translation/Formatter/XliffFormatter.php
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();
}

}
13 changes: 13 additions & 0 deletions src/Symfony/Component/Translation/Formatter/YamlFormatter.php
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);
}
}
0