From a2e7072f12e1538bfbe37d16b2bc6530e99c5223 Mon Sep 17 00:00:00 2001
From: Mark Carver <mark.carver@me.com>
Date: Wed, 10 Jul 2013 11:15:57 -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    | 223 +++++++++++++++++++++
 .../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, 568 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 @@
+<?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 (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<!-- TRANSLATION: ");
+        $compiler->subcompile($msg);
+        if (NULL !== $this->getNode('plural')) {
+          $compiler->raw(', PLURAL: ')->subcompile($msg1);
+        }
+        $compiler->raw(" -->\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<!-- TRANSLATION: ");
+        $compiler->subcompile($msg);
+        if (NULL !== $this->getNode('plural')) {
+          $compiler->raw(', PLURAL: ')->subcompile($msg1);
+        }
+        $compiler->raw(" -->\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 @@
+<?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_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..b2687f1
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php
@@ -0,0 +1,223 @@
+<?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();
+
+    // Enable twig debug and write to the test settings.php file.
+    $this->settingsSet('twig_debug', TRUE);
+    $settings['settings']['twig_debug'] = (object) array(
+      'value' => TRUE,
+      'required' => TRUE,
+    );
+    $this->writeSettings($settings);
+
+    // Rebuild the service container and clear all caches.
+    $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(
+      '<!-- TRANSLATION: "Hello star.", PLURAL: "Hello @count stars." -->',
+      'The "twig_debug" translation comment markup printed successfully for the above test.'
+    );
+
+    $this->assertRaw(
+      'ESCAPEE: &amp;&quot;&lt;&gt;',
+      '{{ token }} was successfully translated and prefixed with "@".'
+    );
+
+    $this->assertRaw(
+      '<!-- TRANSLATION: "Escaped: @string" -->',
+      '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(
+      '<!-- TRANSLATION: "Pass-through: !string" -->',
+      'The "twig_debug" translation comment markup printed successfully for the above test.'
+    );
+
+    $this->assertRaw(
+      'PLAYSHOLDR: <em class="placeholder">&amp;&quot;&lt;&gt;</em>',
+      '{{ token|placeholder }} was successfully translated and prefixed with "%".'
+    );
+
+    $this->assertRaw(
+      '<!-- TRANSLATION: "Placeholder: %string" -->',
+      'The "twig_debug" translation comment markup printed successfully for the above test.'
+    );
+
+    $this->assertRaw(
+      'DIS complex token HAZ LENGTH OV: 3. IT CONTAYNZ: <em class="placeholder">12345</em> AN &amp;&quot;&lt;&gt;. LETS PAS TEH BAD TEXT THRU: &"<>.',
+      '{{ complex.tokens }} were successfully translated with appropriate prefixes.'
+    );
+
+    $this->assertRaw(
+      '<!-- TRANSLATION: "This @name has a length of: @count. It contains: %numbers and @bad_text. Lets pass the bad text through: !bad_text." -->',
+      '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. #}
+<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>
+
+{% set token = {'name': 'complex token', 'numbers': '12345', 'bad_text': '&"<>' } %}
+{% set count = token|length %}
+<div>
+  {% 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 %}
+</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

