pax_global_header00006660000000000000000000000064136713502670014524gustar00rootroot0000000000000052 comment=1f509fa3c3f66551e1f4a346e4477c6c0dc76f9e twig-i18n-extension-3.0.0/000077500000000000000000000000001367135026700152655ustar00rootroot00000000000000twig-i18n-extension-3.0.0/CHANGELOG.md000066400000000000000000000006101367135026700170730ustar00rootroot00000000000000# Change Log ## [Unreleased] - ## [3.0.0] - 2020-06-14 * Add a .gitattributes file * Support Twig 3 * Remove extra field from composer.json * Add support field in composer.json * Require php >= 7.1 * Setup and apply phpmyadmin/coding-standard * Apply changes for php 8.0 compatibility (https://github.com/twigphp/Twig/issues/3327) ## [2.0.0] - 2020-01-14 * First release of this library.twig-i18n-extension-3.0.0/LICENSE000066400000000000000000000021241367135026700162710ustar00rootroot00000000000000Copyright (c) 2010-2019 Fabien Potencier Copyright (c) 2019 phpMyAdmin contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. twig-i18n-extension-3.0.0/README.rst000066400000000000000000000144721367135026700167640ustar00rootroot00000000000000Twig i18n Extension =================== The ``i18n`` extension adds `gettext`_ support to Twig. It defines one tag, ``trans``. Installation ------------ This library can be installed via Composer running the following from the command line: .. code-block:: bash composer require phpmyadmin/twig-i18n-extension Configuration ------------- You need to register this extension before using the ``trans`` block .. code-block:: php use PhpMyAdmin\Twig\Extensions\I18nExtension; $twig->addExtension(new I18nExtension()); Note that you must configure the ``gettext`` extension before rendering any internationalized template. Here is a simple configuration example from the PHP `documentation`_ .. code-block:: php // Set language to French putenv('LC_ALL=fr_FR'); setlocale(LC_ALL, 'fr_FR'); // Specify the location of the translation tables bindtextdomain('myAppPhp', 'includes/locale'); bind_textdomain_codeset('myAppPhp', 'UTF-8'); // Choose domain textdomain('myAppPhp'); .. caution:: The ``i18n`` extension only works if the PHP `gettext`_ extension is enabled. Usage ----- Use the ``trans`` block to mark parts in the template as translatable: .. code-block:: twig {% trans "Hello World!" %} {% trans string_var %} {% trans %} Hello World! {% endtrans %} In a translatable string, you can embed variables: .. code-block:: twig {% trans %} Hello {{ name }}! {% endtrans %} During the gettext lookup these placeholders are converted. ``{{ name }}`` becomes ``%name%`` so the gettext ``msgid`` for this string would be ``Hello %name%!``. .. note:: ``{% trans "Hello {{ name }}!" %}`` is not a valid statement. If you need to apply filters to the variables, you first need to assign the result to a variable: .. code-block:: twig {% set name = name|capitalize %} {% trans %} Hello {{ name }}! {% endtrans %} To pluralize a translatable string, use the ``plural`` block: .. code-block:: twig {% trans %} Hey {{ name }}, I have one apple. {% plural apple_count %} Hey {{ name }}, I have {{ count }} apples. {% endtrans %} The ``plural`` tag should provide the ``count`` used to select the right string. Within the translatable string, the special ``count`` variable always contain the count value (here the value of ``apple_count``). To add notes for translators, use the ``notes`` block: .. code-block:: twig {% trans %} Hey {{ name }}, I have one apple. {% plural apple_count %} Hey {{ name }}, I have {{ count }} apples. {% notes %} This is shown in the user menu. This string should be shorter than 30 chars {% endtrans %} You can use ``notes`` with or without ``plural``. Once you get your templates compiled you should configure the ``gettext`` parser to get something like this: ``xgettext --add-comments=notes`` Within an expression or in a tag, you can use the ``trans`` filter to translate simple strings or variables: .. code-block:: twig {{ var|default(default_value|trans) }} Complex Translations within an Expression or Tag ------------------------------------------------ Translations can be done with both the ``trans`` tag and the ``trans`` filter. The filter is less powerful as it only works for simple variables or strings. For more complex scenario, like pluralization, you can use a two-step strategy: .. code-block:: twig {# assign the translation to a temporary variable #} {% set default_value %} {% trans %} Hey {{ name }}, I have one apple. {% plural apple_count %} Hey {{ name }}, I have {{ count }} apples. {% endtrans %} {% endset %} {# use the temporary variable within an expression #} {{ var|default(default_value|trans) }} Extracting Template Strings --------------------------- If you use the Twig I18n extension, you will probably need to extract the template strings at some point. Using Poedit 2 ~~~~~~~~~~~~~~ Poedit 2 has native support for extracting from Twig files and no extra setup is necessary (Pro version). Using ``xgettext`` or Poedit 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unfortunately, the ``xgettext`` utility does not understand Twig templates natively and neither do tools based on it such as free versions of Poedit. But there is a simple workaround: as Twig converts templates to PHP files, you can use ``xgettext`` on the template cache instead. Create a script that forces the generation of the cache for all your templates. Here is a simple example to get you started .. code-block:: php use Twig\Environment; use Twig\Loader\FilesystemLoader; use PhpMyAdmin\Twig\Extensions\I18nExtension; $tplDir = __DIR__ . '/templates'; $tmpDir = '/tmp/cache/'; $loader = new FilesystemLoader($tplDir); // force auto-reload to always have the latest version of the template $twig = new Environment($loader, [ 'auto_reload' => true, 'cache' => $tmpDir, ]); $twig->addExtension(new I18nExtension()); // configure Twig the way you want // iterate over all your templates foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tplDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { // force compilation if ($file->isFile()) { $twig->loadTemplate(str_replace($tplDir . '/', '', $file)); } } Use the standard ``xgettext`` utility as you would have done with plain PHP code: .. code-block:: text xgettext --default-domain=messages -p ./locale --from-code=UTF-8 -n --omit-header -L PHP /tmp/cache/*.php Another workaround is to use `Twig Gettext Extractor`_ and extract the template strings right from `Poedit`_. .. _`gettext`: https://www.php.net/gettext .. _`documentation`: https://www.php.net/manual/en/function.gettext.php .. _`Twig Gettext Extractor`: https://github.com/umpirsky/Twig-Gettext-Extractor#readme .. _`Poedit`: https://poedit.net/ History ------- This project was forked in 2019 by the phpMyAdmin team, since it was abandoned by the `Twig project`_ but was still in use for phpMyAdmin. .. _`Twig project`: https://github.com/twigphp/Twig-extensions If you find this work useful, or have a pull request to contribute, please find us on `Github`_. .. _`Github`: https://github.com/phpmyadmin/twig-i18n-extension/ twig-i18n-extension-3.0.0/composer.json000066400000000000000000000023251367135026700200110ustar00rootroot00000000000000{ "name": "phpmyadmin/twig-i18n-extension", "description": "Internationalization support for Twig via the gettext library", "keywords": ["i18n","gettext"], "type": "library", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "The phpMyAdmin Team", "email": "developers@phpmyadmin.net", "homepage": "https://www.phpmyadmin.net/team/" } ], "support": { "issues": "https://github.com/phpmyadmin/twig-i18n-extension/issues", "source": "https://github.com/phpmyadmin/twig-i18n-extension" }, "require": { "php": ">=7.1", "twig/twig": "^1.42.3|^2.0|^3.0" }, "require-dev": { "phpmyadmin/coding-standard": "^2.0", "phpunit/phpunit": "^7 || ^8 || ^9" }, "scripts": { "phpunit": "phpunit", "phpcs": "phpcs", "phpcbf": "phpcbf" }, "autoload": { "psr-4": { "PhpMyAdmin\\Twig\\Extensions\\": "src/" } }, "autoload-dev": { "psr-4": { "PhpMyAdmin\\Tests\\Twig\\Extensions\\": "test/" } }, "config":{ "sort-packages": true } } twig-i18n-extension-3.0.0/phpunit.xml.dist000066400000000000000000000012241367135026700204370ustar00rootroot00000000000000 ./test/ ./src/ twig-i18n-extension-3.0.0/src/000077500000000000000000000000001367135026700160545ustar00rootroot00000000000000twig-i18n-extension-3.0.0/src/I18nExtension.php000066400000000000000000000015171367135026700212050ustar00rootroot00000000000000 */ class TransNode extends Node { public function __construct(Node $body, ?Node $plural, ?AbstractExpression $count, ?Node $notes, int $lineno = 0, ?string $tag = null) { $nodes = ['body' => $body]; if ($count !== null) { $nodes['count'] = $count; } if ($plural !== null) { $nodes['plural'] = $plural; } if ($notes !== null) { $nodes['notes'] = $notes; } parent::__construct($nodes, [], $lineno, $tag); } /** * {@inheritdoc} */ public function compile(Compiler $compiler) { $compiler->addDebugInfo($this); [$msg, $vars] = $this->compileString($this->getNode('body')); if ($this->hasNode('plural')) { [$msg1, $vars1] = $this->compileString($this->getNode('plural')); $vars = array_merge($vars, $vars1); } $function = $this->getTransFunction($this->hasNode('plural')); if ($this->hasNode('notes')) { $message = trim($this->getNode('notes')->getAttribute('data')); // line breaks are not allowed cause we want a single line comment $message = str_replace(["\n", "\r"], ' ', $message); $compiler->write('// notes: ' . $message . "\n"); } if ($vars) { $compiler ->write('echo strtr(' . $function . '(') ->subcompile($msg); if ($this->hasNode('plural')) { $compiler ->raw(', ') ->subcompile($msg1) ->raw(', abs(') ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) ->raw(')'); } $compiler->raw('), array('); foreach ($vars as $var) { if ($var->getAttribute('name') === 'count') { $compiler ->string('%count%') ->raw(' => abs(') ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) ->raw('), '); } else { $compiler ->string('%' . $var->getAttribute('name') . '%') ->raw(' => ') ->subcompile($var) ->raw(', '); } } $compiler->raw("));\n"); } else { $compiler ->write('echo ' . $function . '(') ->subcompile($msg); if ($this->hasNode('plural')) { $compiler ->raw(', ') ->subcompile($msg1) ->raw(', abs(') ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) ->raw(')'); } $compiler->raw(");\n"); } } /** * Keep this method protected instead of private * Twig/I18n/NodeTrans from phpmyadmin/phpmyadmin uses it */ protected function compileString(Node $body): array { if ($body instanceof NameExpression || $body instanceof ConstantExpression || $body instanceof TempNameExpression) { return [$body, []]; } $vars = []; if (count($body)) { $msg = ''; foreach ($body as $node) { if ($node instanceof PrintNode) { $n = $node->getNode('expr'); while ($n instanceof FilterExpression) { $n = $n->getNode('node'); } while ($n instanceof CheckToStringNode) { $n = $n->getNode('expr'); } $msg .= sprintf('%%%s%%', $n->getAttribute('name')); $vars[] = new NameExpression($n->getAttribute('name'), $n->getTemplateLine()); } else { $msg .= $node->getAttribute('data'); } } } else { $msg = $body->getAttribute('data'); } return [new Node([new ConstantExpression(trim($msg), $body->getTemplateLine())]), $vars]; } private function getTransFunction(bool $plural): string { return $plural ? 'ngettext' : 'gettext'; } } twig-i18n-extension-3.0.0/src/TokenParser/000077500000000000000000000000001367135026700203115ustar00rootroot00000000000000twig-i18n-extension-3.0.0/src/TokenParser/TransTokenParser.php000066400000000000000000000056771367135026700243060ustar00rootroot00000000000000getLine(); $stream = $this->parser->getStream(); $count = null; $plural = null; $notes = null; if (! $stream->test(Token::BLOCK_END_TYPE)) { $body = $this->parser->getExpressionParser()->parseExpression(); } else { $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); $next = $stream->next()->getValue(); if ($next === 'plural') { $count = $this->parser->getExpressionParser()->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); $plural = $this->parser->subparse([$this, 'decideForFork']); if ($stream->next()->getValue() === 'notes') { $stream->expect(Token::BLOCK_END_TYPE); $notes = $this->parser->subparse([$this, 'decideForEnd'], true); } } elseif ($next === 'notes') { $stream->expect(Token::BLOCK_END_TYPE); $notes = $this->parser->subparse([$this, 'decideForEnd'], true); } } $stream->expect(Token::BLOCK_END_TYPE); $this->checkTransString($body, $lineno); return new TransNode($body, $plural, $count, $notes, $lineno, $this->getTag()); } /** * @return bool */ public function decideForFork(Token $token) { return $token->test(['plural', 'notes', 'endtrans']); } /** * @return bool */ public function decideForEnd(Token $token) { return $token->test('endtrans'); } /** * {@inheritdoc} */ public function getTag() { return 'trans'; } /** * @return void * * @throws SyntaxError */ protected function checkTransString(Node $body, $lineno) { foreach ($body as $i => $node) { if ($node instanceof TextNode || ($node instanceof PrintNode && $node->getNode('expr') instanceof NameExpression) ) { continue; } throw new SyntaxError(sprintf('The text to be translated with "trans" can only contain references to simple variables'), $lineno); } } } twig-i18n-extension-3.0.0/test/000077500000000000000000000000001367135026700162445ustar00rootroot00000000000000twig-i18n-extension-3.0.0/test/Node/000077500000000000000000000000001367135026700171315ustar00rootroot00000000000000twig-i18n-extension-3.0.0/test/Node/TransTest.php000066400000000000000000000116621367135026700215770ustar00rootroot00000000000000assertEquals($body, $node->getNode('body')); $this->assertEquals($count, $node->getNode('count')); $this->assertEquals($plural, $node->getNode('plural')); } public function getTests() { $tests = []; $body = new NameExpression('foo', 0); $node = new TransNode($body, null, null, null, 0); $tests[] = [$node, sprintf('echo gettext(%s);', $this->getVariableGetter('foo'))]; $body = new ConstantExpression('Hello', 0); $node = new TransNode($body, null, null, null, 0); $tests[] = [$node, 'echo gettext("Hello");']; $body = new Node([ new TextNode('Hello', 0), ], [], 0); $node = new TransNode($body, null, null, null, 0); $tests[] = [$node, 'echo gettext("Hello");']; $body = new Node([ new TextNode('J\'ai ', 0), new PrintNode(new NameExpression('foo', 0), 0), new TextNode(' pommes', 0), ], [], 0); $node = new TransNode($body, null, null, null, 0); $tests[] = [$node, sprintf('echo strtr(gettext("J\'ai %%foo%% pommes"), array("%%foo%%" => %s, ));', $this->getVariableGetter('foo'))]; $count = new ConstantExpression(12, 0); $body = new Node([ new TextNode('Hey ', 0), new PrintNode(new NameExpression('name', 0), 0), new TextNode(', I have one apple', 0), ], [], 0); $plural = new Node([ new TextNode('Hey ', 0), new PrintNode(new NameExpression('name', 0), 0), new TextNode(', I have ', 0), new PrintNode(new NameExpression('count', 0), 0), new TextNode(' apples', 0), ], [], 0); $node = new TransNode($body, $plural, $count, null, 0); $tests[] = [$node, sprintf('echo strtr(ngettext("Hey %%name%%, I have one apple", "Hey %%name%%, I have %%count%% apples", abs(12)), array("%%name%%" => %s, "%%name%%" => %s, "%%count%%" => abs(12), ));', $this->getVariableGetter('name'), $this->getVariableGetter('name'))]; // with escaper extension set to on $body = new Node([ new TextNode('J\'ai ', 0), new PrintNode(new FilterExpression(new NameExpression('foo', 0), new ConstantExpression('escape', 0), new Node(), 0), 0), new TextNode(' pommes', 0), ], [], 0); $node = new TransNode($body, null, null, null, 0); $tests[] = [$node, sprintf('echo strtr(gettext("J\'ai %%foo%% pommes"), array("%%foo%%" => %s, ));', $this->getVariableGetter('foo'))]; // with notes $body = new ConstantExpression('Hello', 0); $notes = new TextNode('Notes for translators', 0); $node = new TransNode($body, null, null, $notes, 0); $tests[] = [$node, "// notes: Notes for translators\necho gettext(\"Hello\");"]; $body = new ConstantExpression('Hello', 0); $notes = new TextNode("Notes for translators\nand line breaks", 0); $node = new TransNode($body, null, null, $notes, 0); $tests[] = [$node, "// notes: Notes for translators and line breaks\necho gettext(\"Hello\");"]; $count = new ConstantExpression(5, 0); $body = new TextNode('There is 1 pending task', 0); $plural = new Node([ new TextNode('There are ', 0), new PrintNode(new NameExpression('count', 0), 0), new TextNode(' pending tasks', 0), ], [], 0); $notes = new TextNode('Notes for translators', 0); $node = new TransNode($body, $plural, $count, $notes, 0); $tests[] = [$node, "// notes: Notes for translators\n" . 'echo strtr(ngettext("There is 1 pending task", "There are %count% pending tasks", abs(5)), array("%count%" => abs(5), ));']; return $tests; } }