From ee8f59630d5fc69c94caabf23e5df3df25d7157d Mon Sep 17 00:00:00 2001
From: Mark Carver <mark.carver@me.com>
Date: Mon, 8 Jul 2013 03:03:48 -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    |   8 +
 core/lib/Drupal/Core/Template/TwigNodeTrans.php    | 166 +++++++++++++++++++
 .../Drupal/Core/Template/TwigTransTokenParser.php  |  73 +++++++++
 .../Drupal/system/Tests/Theme/TwigTransTest.php    | 178 +++++++++++++++++++++
 .../twig_theme_test/TwigThemeTestController.php    |  10 ++
 .../templates/twig_theme_test.trans.html.twig      |  45 ++++++
 .../modules/twig_theme_test/twig_theme_test.module |   4 +
 .../twig_theme_test/twig_theme_test.routing.yml    |   6 +
 8 files changed, 490 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..85573d0 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -32,6 +32,13 @@ public function getFunctions() {
   public function getFilters() {
     return array(
       't' => new \Twig_Filter_Function('t'),
+      /**
+       * The "raw" filter is not detectable with the trans tag. Create a faux
+       * filter here to assist so variables are passed-through un-escaped to
+       * the t() function.
+       */
+      'passthrough' => new \Twig_SimpleFilter('passthrough', 'twig_raw_filter'),
+      'placeholder' => new \Twig_SimpleFilter('placeholder', array('String', 'placeholder')),
     );
   }

@@ -47,6 +54,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..f41392e
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Template\TwigNodeTrans.
+ *
+ * @see https://github.com/fabpot/Twig-extensions
+ */
+
+namespace Drupal\Core\Template;
+
+/**
+ * A trans tag.
+ */
+class TwigNodeTrans extends \Twig_Node
+{
+  public function __construct(\Twig_NodeInterface $body, \Twig_NodeInterface $plural = null, \Twig_Node_Expression $count = null, $lineno, $tag = null)
+  {
+    parent::__construct(array('count' => $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 ('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("));\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(");\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);
+
+          $filter = '@';
+
+          if ($args instanceof \Twig_Node_Expression_Filter) {
+            switch ($args->getNode('filter')->getAttribute('value')) {
+              /**
+               * The "raw" filter is not detectable here. Use the duplicated
+               * passthrough filter here to assist so variables are
+               * passed-through un-escaped to the t() function.
+               */
+              case 'passthrough':
+                $filter = '!';
+                break;
+              case 'placeholder':
+                $filter = '%';
+                break;
+            }
+            $args = $args->getNode('node');
+          }
+          $argName = $n->getAttribute('name');
+          if (!is_null($args)) {
+            $argName = $args->getAttribute('name');
+          }
+          $placeholder = sprintf('%s%s', $filter, $argName);
+          $msg .= $placeholder;
+          $expr = new \Twig_Node_Expression_Name($argName, $n->getLine());
+          $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..85d93f9
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Template\TwigTransTokenParser.
+ *
+ * @see https://github.com/fabpot/Twig-extensions
+ */
+
+namespace Drupal\Core\Template;
+
+class TwigTransTokenParser extends \Twig_TokenParser {
+  /**
+   * {@inheritdoc}
+   */
+  public function parse(\Twig_Token $token) {
+    $lineno = $token->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_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..2a646ad
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Theme\TwigTransTest.
+ */
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests twig trans and format_plural blocks.
+ */
+class TwigTransTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'theme_test',
+    'twig_theme_test',
+    'locale',
+    'language'
+  );
+
+  protected $admin_user;
+  protected $langcode = 'xx';
+  protected $name = 'Lolspeak';
+
+  public static function getInfo() {
+    return array(
+      'name' => '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();
+
+    // 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',
+      '"Hello sun." translation tag was successfully translated.'
+    );
+
+    $this->assertText(
+      'OH HAI TEH MUUN',
+      '"Hello moon." translation tag was successfully translated.'
+    );
+
+    $this->assertText(
+      'O HAI STARRRRR',
+      '"Hello star." translation tag with plural count (1) was successfully translated.'
+    );
+
+    $this->assertText(
+      'O HAI 2 STARZZZZ',
+      '"Hello {{ count }} stars." translation tag with plural count (2) was successfully translated.'
+    );
+
+    $this->assertRaw(
+      'ESCAPEE: &amp;&quot;&lt;&gt;',
+      'Translation tag with escaped variable was successfully translated.'
+    );
+
+    $this->assertRaw(
+      'PAS-THRU: &"<>',
+      'Translation tag with pass-through variable was successfully translated.'
+    );
+
+    $this->assertRaw(
+      'PLAYSHOLDR: <em class="placeholder">&amp;&quot;&lt;&gt;</em>',
+      'Translation tag with placeholder variable was successfully translated.'
+    );
+
+  }
+
+  /**
+   * 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"
+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..03295e1
--- /dev/null
+++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig
@@ -0,0 +1,45 @@
+{# Output for the Twig trans block test. #}
+<div>
+  {% trans "Hello sun." %}
+</div>
+
+<div>
+  {% trans %}
+    Hello moon.
+  {% endtrans %}
+</div>
+
+<div>
+  {% set count = 1 %}
+  {% trans %}
+    Hello star.
+  {% plural count %}
+    Hello {{ count }} stars.
+  {% endtrans %}
+</div>
+
+<div>
+  {% set count = 2 %}
+  {% trans %}
+    Hello star.
+  {% plural count %}
+    Hello {{ count }} stars.
+  {% endtrans %}
+</div>
+
+{% set string = '&"<>' %}
+<div>
+  {% trans %}
+    Escaped: {{ string }}
+  {% endtrans %}
+</div>
+<div>
+  {% trans %}
+    Pass-through: {{ string|passthrough }}
+  {% endtrans %}
+</div>
+<div>
+  {% trans %}
+    Placeholder: {{ string|placeholder }}
+  {% endtrans %}
+</div>
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

