From 2c58186f68e7406bbf014c39a1a671ea2ec63326 Mon Sep 17 00:00:00 2001 From: Mark Carver Date: Wed, 10 Jul 2013 02:18:29 -0500 Subject: Issue #1927584 by Mark Carver, Cottser, drupalninja99, jenlampton, John Bickar, geoffreyr, ezeedub: Handle trans block as Twig extension --- core/lib/Drupal/Core/Template/TwigExtension.php | 9 + core/lib/Drupal/Core/Template/TwigNodeTrans.php | 188 ++++++++++++++++++ .../Drupal/Core/Template/TwigTransTokenParser.php | 75 +++++++ .../Drupal/system/Tests/Theme/TwigTransTest.php | 216 +++++++++++++++++++++ .../twig_theme_test/TwigThemeTestController.php | 10 + .../templates/twig_theme_test.trans.html.twig | 53 +++++ .../modules/twig_theme_test/twig_theme_test.module | 4 + .../twig_theme_test/twig_theme_test.routing.yml | 6 + 8 files changed, 561 insertions(+) create mode 100644 core/lib/Drupal/Core/Template/TwigNodeTrans.php create mode 100644 core/lib/Drupal/Core/Template/TwigTransTokenParser.php create mode 100644 core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php create mode 100644 core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 3c86bd1..9c981ae 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -32,6 +32,14 @@ public function getFunctions() { public function getFilters() { return array( 't' => new \Twig_Filter_Function('t'), + /** + * Fake filters equivalent to "raw" and only used in the trans tag. + * + * These filters are necessary to identify the type type of prefix to use + * when passing tokens to t() from a trans tag. + */ + 'passthrough' => new \Twig_SimpleFilter('passthrough', 'twig_raw_filter'), + 'placeholder' => new \Twig_SimpleFilter('placeholder', 'twig_raw_filter'), ); } @@ -47,6 +55,7 @@ public function getTokenParsers() { return array( new TwigFunctionTokenParser('hide'), new TwigFunctionTokenParser('show'), + new TwigTransTokenParser(), ); } diff --git a/core/lib/Drupal/Core/Template/TwigNodeTrans.php b/core/lib/Drupal/Core/Template/TwigNodeTrans.php new file mode 100644 index 0000000..fa9f63b --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php @@ -0,0 +1,188 @@ + $count, + 'body' => $body, + 'plural' => $plural + ), array(), $lineno, $tag); + } + + /** + * {@inheritdoc} + */ + public function compile(\Twig_Compiler $compiler) { + $compiler->addDebugInfo($this); + + list($msg, $vars) = $this->compileString($this->getNode('body')); + + if (NULL !== $this->getNode('plural')) { + list($msg1, $vars1) = $this->compileString($this->getNode('plural')); + $vars = array_merge($vars, $vars1); + } + + $function = NULL === $this->getNode('plural') ? 't' : 'format_plural'; + + if ($vars) { + $compiler->write('echo ' . $function . '('); + + // Move count in format_plural to first argument. + if (NULL !== $this->getNode('plural')) { + $compiler + ->raw('abs(') + ->subcompile($this->getNode('count')) + ->raw('),'); + } + + $compiler->subcompile($msg); + + if (NULL !== $this->getNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1); + } + + $compiler->raw(', array('); + + foreach ($vars as $var) { + if (NULL !== $this->getNode('plural') && 'count' === $var->getAttribute('name')) { + $compiler + ->string('@count') + ->raw(' => abs(') + ->subcompile($this->getNode('count')) + ->raw('), '); + } + else { + $compiler + ->string($var->getAttribute('placeholder')) + ->raw(' => ') + ->subcompile($var) + ->raw(', '); + } + } + + $compiler->raw("))"); + if (settings()->get('twig_debug', FALSE)) { + $compiler->raw(" . '\n\n'"); + } + $compiler->raw(";\n"); + } + else { + $compiler->write('echo ' . $function . '('); + + // Move count in format_plural to first argument. + if (NULL !== $this->getNode('plural')) { + $compiler + ->raw('abs(') + ->subcompile($this->getNode('count')) + ->raw('),'); + } + + $compiler->subcompile($msg); + + if (NULL !== $this->getNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1); + } + + $compiler->raw(")"); + + if (settings()->get('twig_debug', FALSE)) { + $compiler->raw(" . '\n\n'"); + } + $compiler->raw(";\n"); + } + } + + protected function compileString(\Twig_NodeInterface $body) { + if ($body instanceof \Twig_Node_Expression_Name || $body instanceof \Twig_Node_Expression_Constant || $body instanceof \Twig_Node_Expression_TempName) { + return array($body, array()); + } + + $vars = array(); + if (count($body)) { + $msg = ''; + + foreach ($body as $node) { + if (get_class($node) === 'Twig_Node' && $node->getNode(0) instanceof \Twig_Node_SetTemp) { + $node = $node->getNode(1); + } + + if ($node instanceof \Twig_Node_Print) { + $n = $node->getNode('expr'); + while ($n instanceof \Twig_Node_Expression_Filter) { + $n = $n->getNode('node'); + } + $args = $n->getNode('arguments')->getNode(0); + + /** + * Default prefix passed to t(), escapes printed token. + */ + $argPrefix = '@'; + + /** + * Detect if one of the "fake" filters for the trans tag is applied. + */ + while ($args instanceof \Twig_Node_Expression_Filter) { + switch ($args->getNode('filter')->getAttribute('value')) { + case 'passthrough': + $argPrefix = '!'; + break; + case 'placeholder': + $argPrefix = '%'; + break; + } + $args = $args->getNode('node'); + } + if ($args instanceof \Twig_Node_Expression_GetAttr) { + $argName = $args->getNode('attribute')->getAttribute('value'); + $expr = $n; + } + else { + $argName = $n->getAttribute('name'); + if (!is_null($args)) { + $argName = $args->getAttribute('name'); + } + $expr = new \Twig_Node_Expression_Name($argName, $n->getLine()); + } + $placeholder = sprintf('%s%s', $argPrefix, $argName); + $msg .= $placeholder; + $expr->setAttribute('placeholder', $placeholder); + $vars[] = $expr; + } + else { + $msg .= $node->getAttribute('data'); + } + } + } + else { + $msg = $body->getAttribute('data'); + } + + return array(new \Twig_Node(array(new \Twig_Node_Expression_Constant(trim($msg), $body->getLine()))), $vars); + } +} diff --git a/core/lib/Drupal/Core/Template/TwigTransTokenParser.php b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php new file mode 100644 index 0000000..d8dd72e --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php @@ -0,0 +1,75 @@ +getLine(); + $stream = $this->parser->getStream(); + $count = NULL; + $plural = NULL; + + if (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) { + $body = $this->parser->getExpressionParser()->parseExpression(); + } + else { + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideForFork')); + if ('plural' === $stream->next()->getValue()) { + $count = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $plural = $this->parser->subparse(array($this, 'decideForEnd'), TRUE); + } + } + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + $this->checkTransString($body, $lineno); + + $node = new TwigNodeTrans($body, $plural, $count, $lineno, $this->getTag()); + + return $node; + } + + public function decideForFork($token) { + return $token->test(array('plural', 'endtrans')); + } + + public function decideForEnd($token) { + return $token->test('endtrans'); + } + + /** + * {@inheritdoc} + */ + public function getTag() { + return 'trans'; + } + + protected function checkTransString(\Twig_NodeInterface $body, $lineno) { + foreach ($body as $i => $node) { + if ( + $node instanceof \Twig_Node_Text + || + ($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_Name) + || + ($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_GetAttr) + || + ($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_Filter) + ) { + continue; + } + throw new \Twig_Error_Syntax(sprintf('The text to be translated with "trans" can only contain references to simple variables'), $lineno); + } + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php new file mode 100644 index 0000000..7f5cfe0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php @@ -0,0 +1,216 @@ + 'Twig Translation', + 'description' => 'Test Twig translation tags.', + 'group' => 'Theme', + ); + } + + protected function setUp() { + parent::setUp(); + + // Setup test_theme. + theme_enable(array('test_theme')); + \Drupal::config('system.theme')->set('default', 'test_theme')->save(); + + // Enable debug, rebuild the service container, and clear all caches. + $this->settingsSet('twig_debug', TRUE); + $this->rebuildContainer(); + $this->resetAll(); + + // Create and log in as admin. + $this->admin_user = $this->drupalCreateUser(array( + 'administer languages', + 'access administration pages', + 'administer site configuration', + 'translate interface' + )); + $this->drupalLogin($this->admin_user); + + // Add test language for translation testing. + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $this->langcode, + 'name' => $this->name, + 'direction' => '0', + ); + + // Install the lolspeak language. + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + $this->assertRaw('"edit-languages-' . $this->langcode . '-weight"', 'Language code found.'); + + // Import a custom .po file for the lolspeak language. + $this->importPoFile($this->examplePoFile(), array( + 'langcode' => $this->langcode, + 'customized' => TRUE, + )); + + // Assign lolspeak to be the default language. + $edit = array('site_default_language' => $this->langcode); + $this->drupalpost('admin/config/regional/settings', $edit, t('Save configuration')); + + // Reset the static cache of the language list. + drupal_static_reset('language_list'); + + // Check that lolspeak is the default language for the site. + $this->assertEqual(language_default()->id, $this->langcode, $this->name . ' is the default language'); + } + + /** + * Test valid Twig "trans" blocks. + */ + public function testTwigTransBlocks() { + $this->drupalGet('twig-theme-test/trans', array('language' => language_load('xx'))); + + $this->assertText( + 'OH HAI SUNZ', + '{% trans "Hello sun." %} was successfully translated.' + ); + + $this->assertText( + 'OH HAI TEH MUUN', + '{% trans %}Hello moon.{% endtrans %} was successfully translated.' + ); + + $this->assertText( + 'O HAI STARRRRR', + '{% trans %} with {% plural count = 1 %} was successfully translated.' + ); + + $this->assertText( + 'O HAI 2 STARZZZZ', + '{% trans %} with {% plural count = 2 %} was successfully translated.' + ); + + $this->assertRaw( + '', + 'The "twig_debug" translation comment markup printed successfully for the above test.' + ); + + $this->assertRaw( + 'ESCAPEE: &"<>', + '{{ token }} was successfully translated and prefixed with "@".' + ); + + $this->assertRaw( + '', + 'The "twig_debug" translation comment markup printed successfully for the above test.' + ); + + $this->assertRaw( + 'PAS-THRU: &"<>', + '{{ token|passthrough }} was successfully translated and prefixed with "!".' + ); + + $this->assertRaw( + '', + 'The "twig_debug" translation comment markup printed successfully for the above test.' + ); + + $this->assertRaw( + 'PLAYSHOLDR: &"<>', + '{{ token|placeholder }} was successfully translated and prefixed with "%".' + ); + + $this->assertRaw( + '', + 'The "twig_debug" translation comment markup printed successfully for the above test.' + ); + + $this->assertRaw( + 'DIS complex token HAZ LENGTH OV: 3. IT CONTAYNZ: 12345 AN &"<>. LETS PAS TEH BAD TEXT THRU: &"<>.', + '{{ complex.tokens }} were successfully translated with appropriate prefixes.' + ); + + $this->assertRaw( + '', + 'The "twig_debug" translation comment markup printed successfully for the above test.' + ); + + } + + /** + * Helper function: import a standalone .po file in a given language. + * Borrowed from Drupal\locale\Tests\LocaleImportFunctionalTest. + * + * @param $contents + * Contents of the .po file to import. + * @param $options + * Additional options to pass to the translation import form. + */ + protected function importPoFile($contents, array $options = array()) { + $name = tempnam('temporary://', "po_") . '.po'; + file_put_contents($name, $contents); + $options['files[file]'] = $name; + $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); + drupal_unlink($name); + } + + protected function examplePoFile() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 8\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "Hello sun." +msgstr "OH HAI SUNZ" + +msgid "Hello moon." +msgstr "OH HAI TEH MUUN" + +msgid "Hello star." +msgid_plural "Hello @count stars." +msgstr[0] "O HAI STARRRRR" +msgstr[1] "O HAI @count STARZZZZ" + +msgid "Escaped: @string" +msgstr "ESCAPEE: @string" + +msgid "Pass-through: !string" +msgstr "PAS-THRU: !string" + +msgid "Placeholder: %string" +msgstr "PLAYSHOLDR: %string" + +msgid "This @name has a length of: @count. It contains: %numbers and @bad_text. Lets pass the bad text through: !bad_text." +msgstr "DIS @name HAZ LENGTH OV: @count. IT CONTAYNZ: %numbers AN @bad_text. LETS PAS TEH BAD TEXT THRU: !bad_text." +EOF; + } + +} diff --git a/core/modules/system/tests/modules/twig_theme_test/lib/Drupal/twig_theme_test/TwigThemeTestController.php b/core/modules/system/tests/modules/twig_theme_test/lib/Drupal/twig_theme_test/TwigThemeTestController.php index ef5fb70..cd809c8 100644 --- a/core/modules/system/tests/modules/twig_theme_test/lib/Drupal/twig_theme_test/TwigThemeTestController.php +++ b/core/modules/system/tests/modules/twig_theme_test/lib/Drupal/twig_theme_test/TwigThemeTestController.php @@ -29,4 +29,14 @@ public function phpVariablesRender() { return theme('twig_theme_test_php_variables'); } + /** + * Menu callback for testing translation blocks in a Twig template. + */ + public function transBlockRender() { + return array( + '#theme' => 'twig_theme_test_trans', + ); + } + + } diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig new file mode 100644 index 0000000..bf2200b --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig @@ -0,0 +1,53 @@ +{# Output for the Twig trans block test. #} +
+ {% trans "Hello sun." %} +
+ +
+ {% trans %} + Hello moon. + {% endtrans %} +
+ +
+ {% set count = 1 %} + {% trans %} + Hello star. + {% plural count %} + Hello {{ count }} stars. + {% endtrans %} +
+ +
+ {% set count = 2 %} + {% trans %} + Hello star. + {% plural count %} + Hello {{ count }} stars. + {% endtrans %} +
+ +{% set string = '&"<>' %} +
+ {% trans %} + Escaped: {{ string }} + {% endtrans %} +
+
+ {% trans %} + Pass-through: {{ string|passthrough }} + {% endtrans %} +
+
+ {% trans %} + Placeholder: {{ string|placeholder }} + {% endtrans %} +
+ +{% set token = {'name': 'complex token', 'numbers': '12345', 'bad_text': '&"<>' } %} +{% set count = token|length %} +
+ {% trans %} + This {{ token.name }} has a length of: {{ count }}. It contains: {{ token.numbers|placeholder }} and {{ token.bad_text }}. Lets pass the bad text through: {{ token.bad_text|passthrough }}. + {% endtrans %} +
diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module index 2ca2cd0..934d1b0 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module @@ -7,6 +7,10 @@ function twig_theme_test_theme($existing, $type, $theme, $path) { $items['twig_theme_test_php_variables'] = array( 'template' => 'twig_theme_test.php_variables', ); + $items['twig_theme_test_trans'] = array( + 'variables' => array(), + 'template' => 'twig_theme_test.trans', + ); return $items; } diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml index cdc0ac1..17ac5b0 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml @@ -4,3 +4,9 @@ twig_theme_test_php_variables: _content: '\Drupal\twig_theme_test\TwigThemeTestController::phpVariablesRender' requirements: _permission: 'access content' +twig_theme_test_trans: + pattern: '/twig-theme-test/trans' + defaults: + _content: '\Drupal\twig_theme_test\TwigThemeTestController::transBlockRender' + requirements: + _permission: 'access content' -- 1.8.2