diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 3abd8fd..5489a29 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -48,6 +48,7 @@ public function getTokenParsers() { new TwigFunctionTokenParser('hide'), new TwigFunctionTokenParser('show'), new TwigTransTokenParser(), + new TwigFormatPluralTokenParser(), ); } diff --git a/core/lib/Drupal/Core/Template/TwigFormatPluralTokenParser.php b/core/lib/Drupal/Core/Template/TwigFormatPluralTokenParser.php new file mode 100644 index 0000000..568735b --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigFormatPluralTokenParser.php @@ -0,0 +1,96 @@ +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->checkFormatPluralString($body, $count, $plural, $lineno); + + $node = new TwigNodeFormatPlural($body, $plural, $count, $lineno, $this->getTag()); + + return $node; + } + + public function decideForFork($token) + { + return $token->test(array('plural', 'endformat_plural')); + } + + public function decideForEnd($token) + { + return $token->test('endformat_plural'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @param string The tag name + */ + public function getTag() + { + return 'format_plural'; + } + + protected function checkFormatPluralString(\Twig_NodeInterface $body, \Twig_NodeInterface $count, \Twig_NodeInterface $plural, $lineno) + { + foreach ($body as $i => $node) { + var_dump(get_class($node)); + /* + if ($node->test('plural')) + { + $locatedPlural = TRUE; + } + */ + if ( + $node instanceof \Twig_Node_Text + || + ($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_Name) + ) { + continue; + } + + throw new \Twig_Error_Syntax(sprintf('The text to be formatted with "format_plural" can only contain references to simple variables'), $lineno); + } + + if (is_null($count) || is_null($plural)) { + throw new \Twig_Error_Syntax(sprintf('The text to be formatted with "format_plural" must contain a "plural" tag.'), $lineno); + } + } +} diff --git a/core/lib/Drupal/Core/Template/TwigNodeFormatPlural.php b/core/lib/Drupal/Core/Template/TwigNodeFormatPlural.php new file mode 100644 index 0000000..7c17beb --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigNodeFormatPlural.php @@ -0,0 +1,148 @@ + $count, 'body' => $body, 'plural' => $plural), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler A Twig_Compiler instance + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + + list($msg, $vars) = $this->compileString($this->getNode('body')); + + list($msg1, $vars1) = $this->compileString($this->getNode('plural')); + $vars = array_merge($vars, $vars1); + + $function = '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('name').'%') + ->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"); + } + + // dpm($compiler->getSource()); + } + + 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); + + $argName = $n->getAttribute('name'); + if (!is_null($args)) { + $argName = $args->getAttribute('name'); + } + + $msg .= sprintf('%%%s%%', $argName); + $vars[] = new \Twig_Node_Expression_Name($argName, $n->getLine()); + } 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/TwigNodeTrans.php b/core/lib/Drupal/Core/Template/TwigNodeTrans.php index 103d0a8..caf3741 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeTrans.php +++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php @@ -40,18 +40,23 @@ public function compile(\Twig_Compiler $compiler) $function = null === $this->getNode('plural') ? 't' : 'format_plural'; if ($vars) { - $compiler - ->write('echo '.$function.'(') - ->subcompile($msg) - ; + $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) - ->raw(', abs(') - ->subcompile($this->getNode('count')) - ->raw(')') ; } @@ -77,23 +82,30 @@ public function compile(\Twig_Compiler $compiler) $compiler->raw("));\n"); } else { - $compiler - ->write('echo '.$function.'(') - ->subcompile($msg) - ; + $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) - ->raw(', abs(') - ->subcompile($this->getNode('count')) - ->raw(')') ; } $compiler->raw(");\n"); } + + // dpm($compiler->getSource()); } protected function compileString(\Twig_NodeInterface $body) @@ -116,8 +128,15 @@ protected function compileString(\Twig_NodeInterface $body) while ($n instanceof \Twig_Node_Expression_Filter) { $n = $n->getNode('node'); } - $msg .= sprintf('%%%s%%', $n->getAttribute('name')); - $vars[] = new \Twig_Node_Expression_Name($n->getAttribute('name'), $n->getLine()); + $args = $n->getNode('arguments')->getNode(0); + + $argName = $n->getAttribute('name'); + if (!is_null($args)) { + $argName = $args->getAttribute('name'); + } + + $msg .= sprintf('%%%s%%', $argName); + $vars[] = new \Twig_Node_Expression_Name($argName, $n->getLine()); } else { $msg .= $node->getAttribute('data'); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransFormatPluralTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransFormatPluralTest.php new file mode 100644 index 0000000..4a561d4 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigTransFormatPluralTest.php @@ -0,0 +1,171 @@ + 'Twig Translation', + 'description' => 'Test Twig translation blocks.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp(); + theme_enable(array('test_theme', 'twig_theme_test')); + } + + /** + * Test valid trans blocks. + */ + function testTransBlocks() { + config('system.theme') + ->set('default', 'test_theme_twig') + ->save(); + + // Log in as admin. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'administer site configuration', 'translate interface')); + $this->drupalLogin($admin_user); + + // Enable lolspeak. + $lolspeak_langcode = 'xx-lolspeak'; + $langcode = 'xx'; + $name = 'Lolspeak'; + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertRaw('"edit-languages-' . $langcode .'-weight"', 'Language code found.'); + + // Assign Lolspeak to be the default language. + $edit = array('site_default_language' => $langcode); + $this->drupalpost('admin/config/regional/settings', $edit, t('Save configuration')); + $this->assertEqual(language_default()->langcode, $langcode, $name . ' is the default language'); + + // Import custom lolspeak .po file. + $this->importPoFile($this->examplePoFile(), array( + 'langcode' => $langcode, + 'customized' => TRUE, + )); + + $this->drupalLogout(); + drupal_language_initialize(); + + $out = $this->drupalGet('twig-theme-test/trans'); + $this->assertText($out, $out); + $this->assertText( + 'O HAI SUNZ', + '"Hello sun." translation tag was successfully translated.' + ); + $this->assertText( + 'O HAI TEH MUUN', + '"Hello moon." translation block was successfully translated.' + ); + /* + @TODO fix these tests. + $this->assertText( + '"Hello star." translation block with variable was successfully translated.', + 'O HAI STARRRRR' + ); + $this->assertText( + '"Hello {{ count }} stars." translation block with variable was successfully translated.', + 'O HAI 2 STARZZZZ' + ); + */ + $this->assertText( + 'O HAI THAR Earth', + '"Hello {{ name }}." translation block with variable was successfully translated.' + ); + } + + /** + * Test valid format_plural blocks. + */ + function testFormatPluralBlocks() { + config('system.theme') + ->set('default', 'test_theme_twig') + ->save(); + + // @TODO: Set up lolspeak crap + + $out = $this->drupalGet('twig-theme-test/format-plural'); + $this->assertText( + 'Hello planet.', + '"Hello planet." format_plural tag was successfully populated.' + ); + $this->assertRaw( + 'Hello 2 planets.', + '"Hello {{ count }} planets." format_plural block was successfully populated.' + ); + } + + /** + * Helper function: import a standalone .po file in a given language. + * Borrowed from Drupal\locale\Tests\LocaleImportFunctionalTest. + * @TODO: Find way to import directly so I don't repeat myself. + * + * @param $contents + * Contents of the .po file to import. + * @param $options + * Additional options to pass to the translation import form. + */ + function importPoFile($contents, array $options = array()) { + $name = tempnam('temporary://', "po_") . '.po'; + file_put_contents($name, $contents); + $options['files[file]'] = $name; + $out = $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); + drupal_unlink($name); + } + + 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 "Earth" +msgstr "TEH ERF" +msgid "Hello star." +msgstr "O HAI STARRRRR" +msgid "Hello %star_numbers% stars." +msgstr "O HAI %star_numbers% STARZZZZ" +msgid "Hello %name%." +msgstr "O HAI THAR %name%" + +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 295f8ca..32514a8 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,32 @@ public function phpVariablesRender() { return theme('twig_theme_test_php_variables'); } + /** + * Menu callback for testing translation blocks in a Twig template. + */ + public function transBlockRender() { + return theme('twig_theme_test_trans'); + } + + /** + * Menu callback for testing invalid translation blocks in a Twig template. + */ + public function transInvalidBlockRender() { + return theme('twig_theme_test_trans_invalid'); + } + + /** + * Menu callback for testing format_plural blocks in a Twig template. + */ + public function formatPluralBlockRender() { + return theme('twig_theme_test_format_plural'); + } + + /** + * Menu callback for testing invalid format_plural blocks in a Twig template. + */ + public function formatPluralInvalidBlockRender() { + return theme('twig_theme_test_format_plural_invalid'); + } + } diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural.html.twig new file mode 100644 index 0000000..e3adf3b --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural.html.twig @@ -0,0 +1,19 @@ +{# Output for Twig Theme format plural test. #} + +
+{% set planet_number = 1 %} +{% format_plural %} + Hello planet. +{% plural planet_number %} + Hello {{ count }} planets. +{% endformat_plural %} +
+ +
+{% set planets_number = 2 %} +{% format_plural %} + Hello planet. +{% plural planets_number %} + Hello {{ count }} planets. +{% endformat_plural %} +
diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural_invalid.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural_invalid.html.twig new file mode 100644 index 0000000..aae7919 --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.format_plural_invalid.html.twig @@ -0,0 +1,17 @@ +{# Output for Twig Theme format plural test. #} + +
+{% set planet_number = 1 %} +{% format_plural %} + Hello planet. +{% endformat_plural %} +
+ +
+{% set planets_number = 2 %} +{% format_plural %} + Hello planet. +{% plural %} + Hello {{ count }} planets. +{% endformat_plural %} +
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..0540acb --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig @@ -0,0 +1,35 @@ +{# Output for Twig Theme translation test. #} +
+ {% trans "Hello sun." %} +
+ +
+{% trans %} +Hello moon. +{% endtrans %} +
+ +
+{% set star_number = 1 %} +{% trans %} +Hello star. +{% plural star_number %} +Hello {{ count }} stars. +{% endtrans %} +
+ +
+{% set star_numbers = 2 %} +{% trans %} +Hello star. +{% plural star_numbers %} +Hello {{ count }} stars. +{% endtrans %} +
+ +
+{% set name = 'Earth' %} +{% trans %} +Hello {{ name }}. +{% endtrans %} +
diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.tpl.php b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.tpl.php new file mode 100644 index 0000000..f317040 --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.tpl.php @@ -0,0 +1,3 @@ +

+ If you can see this, phptemplate is being used and the test has failed. +

diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans_invalid.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans_invalid.html.twig new file mode 100644 index 0000000..3b5e27d --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans_invalid.html.twig @@ -0,0 +1,24 @@ +{# Output for invalid Twig Theme translation test. #} +
+ {% trans %} +
+ +
+{% trans %}{% endtrans %} +
+ +
+{% set star_number = 1 %} +{% trans %} + Hello star. +{% plural %} + Hello {{ count }} stars. +{% endtrans %} +
+ +
+{% set star_numbers = 2 %} +{% trans %} + Hello star. +{% plural star_numbers %}{% 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..7e49f90 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,18 @@ 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( + 'template' => 'twig_theme_test.trans', + ); + $items['twig_theme_test_trans_invalid'] = array( + 'template' => 'twig_theme_test.trans_invalid', + ); + $items['twig_theme_test_format_plural'] = array( + 'template' => 'twig_theme_test.format_plural', + ); + $items['twig_theme_test_format_plural_invalid'] = array( + 'template' => 'twig_theme_test.format_plural_invalid', + ); 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..531a1d0 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,27 @@ 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' +twig_theme_test_trans_invalid: + pattern: '/twig-theme-test/trans/invalid' + defaults: + _content: '\Drupal\twig_theme_test\TwigThemeTestController::transInvalidBlockRender' + requirements: + _permission: 'access content' +twig_theme_test_format_plural: + pattern: '/twig-theme-test/format-plural' + defaults: + _content: '\Drupal\twig_theme_test\TwigThemeTestController::formatPluralBlockRender' + requirements: + _permission: 'access content' +twig_theme_test_format_plural_invalid: + pattern: '/twig-theme-test/format-plural/invalid' + defaults: + _content: '\Drupal\twig_theme_test\TwigThemeTestController::formatPluralInvalidBlockRender' + requirements: + _permission: 'access content'