From da8a7599c5e80be210ae896501b9366d5ccca47c Mon Sep 17 00:00:00 2001
From: Mark Carver <mark.carver@me.com>
Date: Thu, 18 Jul 2013 13:53:11 -0500
Subject: Issue #1927584 by Mark Carver, drupalninja99, penyaskito, Cottser,
 jenlampton, John Bickar, geoffreyr, ezeedub: Add support for the Twig {%
 trans %} tag extension

---
 core/lib/Drupal/Core/Template/TwigExtension.php    |   9 +
 core/lib/Drupal/Core/Template/TwigNodeTrans.php    | 168 +++++++++++++
 .../Drupal/Core/Template/TwigTransTokenParser.php  | 103 ++++++++
 .../Drupal/system/Tests/Theme/TwigTransTest.php    | 274 +++++++++++++++++++++
 .../twig_theme_test/TwigThemeTestController.php    |   9 +
 .../templates/twig_theme_test.trans.html.twig      |  63 +++++
 .../modules/twig_theme_test/twig_theme_test.module |   4 +
 .../twig_theme_test/twig_theme_test.routing.yml    |   6 +
 8 files changed, 636 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..9cd17b0 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'),
+      'trans' => new \Twig_Filter_Function('t'),
+      // The "raw" filter is not detectable when parsing "trans" tags. To detect
+      // which prefix must be used for translation (@, !, %), we must clone the
+      // "raw" filter and give it identifiable names. These filters should only
+      // be used in "trans" tags.
+      // @see TwigNodeTrans::compileString()
+      'passthrough' => new \Twig_Filter_Function('twig_raw_filter'),
+      'placeholder' => new \Twig_Filter_Function('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..ee32e98
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php
@@ -0,0 +1,168 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Template\TwigNodeTrans.
+ *
+ * This Twig extension was originally based on Twig i18n extension. It has been
+ * severely modified to work properly with the complexities of the Drupal
+ * translation system.
+ *
+ * @see http://twig.sensiolabs.org/doc/extensions/i18n.html
+ * @see https://github.com/fabpot/Twig-extensions
+ */
+
+namespace Drupal\Core\Template;
+
+/**
+ * A class that defines the Twig 'trans' tag for Drupal.
+ */
+class TwigNodeTrans extends \Twig_Node {
+
+  /**
+   * {@inheritdoc}
+   */
+  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($singular, $tokens) = $this->compileString($this->getNode('body'));
+    $plural = NULL;
+
+    if (NULL !== $this->getNode('plural')) {
+      list($plural, $pluralTokens) = $this->compileString($this->getNode('plural'));
+      $tokens = array_merge($tokens, $pluralTokens);
+    }
+
+    // Start writing with the function to be called.
+    $compiler->write('echo ' . (empty($plural) ? 't' : 'format_plural') . '(');
+
+    // Move the count to the beginning of the parameters list.
+    if (!empty($plural)) {
+      $compiler->raw('abs(')->subcompile($this->getNode('count'))->raw('), ');
+    }
+
+    // Write the singular text parameter.
+    $compiler->subcompile($singular);
+
+    // Write the plural text parameter, if necessary.
+    if (!empty($plural)) {
+      $compiler->raw(', ')->subcompile($plural);
+    }
+
+    // Write any tokens found as an associative array parameter.
+    if (!empty($tokens)) {
+      $compiler->raw(', array(');
+      foreach ($tokens as $token) {
+        $compiler->string($token->getAttribute('placeholder'))->raw(' => ')->subcompile($token)->raw(', ');
+      }
+      $compiler->raw(')');
+    }
+
+    // Write function closure.
+    $compiler->raw(')');
+
+    // Append translation debug markup, if necessary.
+    if (settings()->get('twig_debug', FALSE)) {
+      $compiler->raw(" . '\n<!-- TRANSLATION: ");
+      $compiler->subcompile($singular);
+      if (!empty($plural)) {
+        $compiler->raw(', PLURAL: ')->subcompile($plural);
+      }
+      $compiler->raw(" -->\n'");
+    }
+
+    // End writing.
+    $compiler->raw(";\n");
+  }
+
+  /**
+   * Extracts the text and tokens for the "trans" tag.
+   *
+   * @param \Twig_NodeInterface $body
+   *   The node to compile.
+   *
+   * @return array
+   *   Returns an array containing the two following parameters:
+   *   - string $text
+   *       The extracted text.
+   *   - array $tokens
+   *       The extracted tokens as new \Twig_Node_Expression_Name instances.
+   */
+  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());
+    }
+
+    $tokens = array();
+    if (count($body)) {
+      $text = '';
+
+      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);
+
+          // Detect if a token implements one of the filters reserved for
+          // modifying the prefix of a token. The default prefix used for
+          // translations is "@". This escapes the printed token and makes them
+          // safe for templates.
+          // @see TwigExtension::getFilters()
+          $argPrefix = '@';
+          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);
+          $text .= $placeholder;
+          $expr->setAttribute('placeholder', $placeholder);
+          $tokens[] = $expr;
+        }
+        else {
+          $text .= $node->getAttribute('data');
+        }
+      }
+    }
+    else {
+      $text = $body->getAttribute('data');
+    }
+
+    return array(new \Twig_Node(array(new \Twig_Node_Expression_Constant(trim($text), $body->getLine()))), $tokens);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Template/TwigTransTokenParser.php b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php
new file mode 100644
index 0000000..44f6d21
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Template\TwigTransTokenParser.
+ *
+ * @see http://twig.sensiolabs.org/doc/extensions/i18n.html
+ * @see https://github.com/fabpot/Twig-extensions
+ */
+
+namespace Drupal\Core\Template;
+
+/**
+ * A class that defines the Twig 'trans' token parser for Drupal.
+ *
+ * The token parser converts a token stream created from template source
+ * code into an Abstract Syntax Tree (AST).  The AST will later be compiled
+ * into PHP code usable for runtime execution of the template.
+ *
+ * @see \Twig_TokenParser
+ */
+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;
+  }
+
+  /**
+   * Detect a 'plural' switch or the end of a 'trans' tag.
+   */
+  public function decideForFork($token) {
+    return $token->test(array('plural', 'endtrans'));
+  }
+
+  /**
+   * Detect the end of a 'trans' tag.
+   */
+  public function decideForEnd($token) {
+    return $token->test('endtrans');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTag() {
+    return 'trans';
+  }
+
+  /**
+   * Ensure that any nodes that are parsed are only of allowed types.
+   *
+   * @param \Twig_NodeInterface $body
+   *   The expression to check.
+   * @param integer $lineno
+   *   The source line.
+   *
+   * @throws \Twig_Error_Syntax
+   */
+  protected function checkTransString(\Twig_NodeInterface $body, $lineno) {
+    foreach ($body as $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..a1ae6d8
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransTest.php
@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Theme\TwigTransTest.
+ */
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests Twig "trans" tags.
+ */
+class TwigTransTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'theme_test',
+    'twig_theme_test',
+    'locale',
+    'language'
+  );
+
+  /**
+   * An administrative user for testing.
+   *
+   * @var \Drupal\user\Plugin\Core\Entity\User
+   */
+  protected $admin_user;
+
+  /**
+   * Custom language code.
+   *
+   * @var string
+   */
+  protected $langcode = 'xx';
+
+  /**
+   * Custom language name.
+   *
+   * @var string
+   */
+  protected $name = 'Lolspeak';
+
+  /**
+   * Defines information about this test.
+   *
+   * @return array
+   *   An associative array of information.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Twig Translation',
+      'description' => 'Test Twig "trans" tags.',
+      'group' => 'Theme',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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 Twig "trans" tags.
+   */
+  public function testTwigTransTags() {
+    $this->drupalGet('twig-theme-test/trans', array('language' => language_load('xx')));
+
+    $this->assertText(
+      'OH HAI SUNZ',
+      '{% trans "Hello sun." %} was successfully translated.'
+    );
+
+    $this->assertText(
+      'O HERRO ERRRF.',
+      '{{ "Hello Earth."|trans }} 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(
+      'ESCAPEE: &amp;&quot;&lt;&gt;',
+      '{{ token }} was successfully translated and prefixed with "@".'
+    );
+
+    $this->assertRaw(
+      'PAS-THRU: &"<>',
+      '{{ token|passthrough }} was successfully translated and prefixed with "!".'
+    );
+
+    $this->assertRaw(
+      'PLAYSHOLDR: <em class="placeholder">&amp;&quot;&lt;&gt;</em>',
+      '{{ token|placeholder }} was successfully translated and prefixed with "%".'
+    );
+
+    $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.'
+    );
+
+    // Ensure debug output does not print.
+    $this->checkForDebugMarkup(FALSE);
+  }
+
+  /**
+   * Test Twig "trans" debug markup.
+   */
+  public function testTwigTransDebug() {
+    // 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);
+
+    // Get page for assertion testing.
+    $this->drupalGet('twig-theme-test/trans', array('language' => language_load('xx')));
+
+    // Ensure debug output is printed.
+    $this->checkForDebugMarkup(TRUE);
+  }
+
+  /**
+   * Helper function: test twig debug translation markup.
+   *
+   * @param bool $visible
+   *   Toggle determining which assertion to use for test.
+   */
+  protected function checkForDebugMarkup($visible) {
+    $tests = array(
+      '{% trans "Hello sun." %}' => '<!-- TRANSLATION: "Hello sun." -->',
+      '{{ "Hello moon."|trans }}' => '<!-- TRANSLATION: "Hello moon." -->',
+      '{% trans %} with {% plural %}' => '<!-- TRANSLATION: "Hello star.", PLURAL: "Hello @count stars." -->',
+      '{{ token }}' => '<!-- TRANSLATION: "Escaped: @string" -->',
+      '{{ token|passthrough }}' => '<!-- TRANSLATION: "Pass-through: !string" -->',
+      '{{ token|placeholder }}' => '<!-- TRANSLATION: "Placeholder: %string" -->',
+      '{{ complex.tokens }}' => '<!-- TRANSLATION: "This @name has a length of: @count. It contains: %numbers and @bad_text. Lets pass the bad text through: !bad_text." -->',
+    );
+    foreach ($tests as $test => $markup) {
+      if ($visible) {
+        $this->assertRaw($markup, "Twig debug translation markup exists in source for: $test");
+      }
+      else {
+        $this->assertNoRaw($markup, "Twig debug translation markup does not exist in source for: $test");
+      }
+    }
+  }
+
+  /**
+   * Helper function: import a standalone .po file in a given language.
+   *
+   * Borrowed from \Drupal\locale\Tests\LocaleImportFunctionalTest.
+   *
+   * @param string $contents
+   *   Contents of the .po file to import.
+   * @param array $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);
+  }
+
+  /**
+   * An example .po file.
+   *
+   * @return string
+   *   The .po contents used for this test.
+   */
+  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 Earth."
+msgstr "O HERRO ERRRF."
+
+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..6eb318a 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,13 @@ 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..f487e5a
--- /dev/null
+++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig
@@ -0,0 +1,63 @@
+{# Test trans tag with string argument. #}
+<div>
+  {% trans 'Hello sun.' %}
+</div>
+
+{# Test trans filter. #}
+<div>
+  {{ 'Hello Earth.'|trans }}
+</div>
+
+{# Test trans tag with text content. #}
+<div>
+  {% trans %}
+    Hello moon.
+  {% endtrans %}
+</div>
+
+{# Test trans/plural tag where count = 1. #}
+<div>
+  {% set count = 1 %}
+  {% trans %}
+    Hello star.
+  {% plural count %}
+    Hello {{ count }} stars.
+  {% endtrans %}
+</div>
+
+{# Test trans/plural tag where count = 2. #}
+<div>
+  {% set count = 2 %}
+  {% trans %}
+    Hello star.
+  {% plural count %}
+    Hello {{ count }} stars.
+  {% endtrans %}
+</div>
+
+{# Test trans tag with different filters applied to tokens. #}
+{% set string = '&"<>' %}
+<div>
+  {% trans %}
+    Escaped: {{ string }}
+  {% endtrans %}
+</div>
+<div>
+  {% trans %}
+    Pass-through: {{ string|passthrough }}
+  {% endtrans %}
+</div>
+<div>
+  {% trans %}
+    Placeholder: {{ string|placeholder }}
+  {% endtrans %}
+</div>
+
+{# Test trans tag with complex tokens. #}
+{% 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

