├── Twig └── Gettext │ ├── Test │ ├── Fixtures │ │ └── twig │ │ │ ├── empty.twig │ │ │ ├── plural.twig │ │ │ ├── singular.twig │ │ │ └── customFunctions.twig │ └── ExtractorTest.php │ ├── Loader │ └── Filesystem.php │ ├── Routing │ └── Generator │ │ └── UrlGenerator.php │ └── Extractor.php ├── .gitignore ├── .travis.yml ├── phpunit.xml.dist ├── box.json ├── composer.json ├── LICENSE ├── twig-gettext-extractor └── README.md /Twig/Gettext/Test/Fixtures/twig/empty.twig: -------------------------------------------------------------------------------- 1 | Nothing to translate here. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | vendor 3 | phpunit.xml 4 | composer.lock 5 | twig-gettext-extractor.phar 6 | -------------------------------------------------------------------------------- /Twig/Gettext/Test/Fixtures/twig/plural.twig: -------------------------------------------------------------------------------- 1 | {% trans %} 2 | Hey {{ name }}, I have one apple. 3 | {% plural apple_count %} 4 | Hey {{ name }}, I have {{ count }} apples. 5 | {% endtrans %} 6 | -------------------------------------------------------------------------------- /Twig/Gettext/Test/Fixtures/twig/singular.twig: -------------------------------------------------------------------------------- 1 | {% trans "Hello World!" %} 2 | 3 | {% trans %} 4 | Hello World! 5 | {% endtrans %} 6 | 7 | {% trans %} 8 | Hello {{ name }}! 9 | {% endtrans %} 10 | -------------------------------------------------------------------------------- /Twig/Gettext/Test/Fixtures/twig/customFunctions.twig: -------------------------------------------------------------------------------- 1 | Welcome {{ first_name }}! 2 | 3 | token: 4 | {{ serverUrl() ~ url('verify_email') ~ email_verify_token }}! 5 | 6 |

link: {{ serverUrl() ~ url('login') }}

-------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | before_script: 8 | - sudo apt-get -qq update 9 | - sudo apt-get install -y gettext 10 | - composer install --dev 11 | 12 | script: 13 | - phpunit 14 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | ./Twig/Gettext/Test/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "directories": ["."], 3 | "finder": [ 4 | { 5 | "name": "*.php", 6 | "exclude": [ 7 | ".gitignore", 8 | ".md", 9 | "phpunit", 10 | "Tests", 11 | "tests", 12 | "yaml" 13 | ], 14 | "in": "vendor" 15 | } 16 | ], 17 | "compactors": [ 18 | "Herrera\\Box\\Compactor\\Json", 19 | "Herrera\\Box\\Compactor\\Php" 20 | ], 21 | "compression": "GZ", 22 | "main": "twig-gettext-extractor", 23 | "output": "twig-gettext-extractor.phar", 24 | "stub": true, 25 | "chmod": "0755" 26 | } 27 | -------------------------------------------------------------------------------- /Twig/Gettext/Loader/Filesystem.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Twig\Gettext\Loader; 13 | 14 | /** 15 | * Loads template from the filesystem. 16 | * 17 | * @author Saša Stamenković 18 | */ 19 | class Filesystem extends \Twig_Loader_Filesystem 20 | { 21 | /** 22 | * Hacked find template to allow loading templates by absolute path. 23 | * 24 | * @param string $name template name or absolute path 25 | */ 26 | protected function findTemplate($name, $throw = null) 27 | { 28 | $result = parent::findTemplate($name, false); 29 | if ($result === false) { 30 | return __DIR__.'/../Test/Fixtures/twig/empty.twig'; 31 | } 32 | return $result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Twig/Gettext/Routing/Generator/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Twig\Gettext\Routing\Generator; 13 | 14 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 15 | use Symfony\Component\Routing\RequestContext; 16 | 17 | /** 18 | * Dummy url generator. 19 | * 20 | * @author Saša Stamenković 21 | */ 22 | class UrlGenerator implements UrlGeneratorInterface 23 | { 24 | protected $context; 25 | 26 | public function generate($name, $parameters = array(), $absolute = false) 27 | { 28 | } 29 | 30 | public function getContext() 31 | { 32 | return $this->context; 33 | } 34 | 35 | public function setContext(RequestContext $context) 36 | { 37 | $this->context = $context; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umpirsky/twig-gettext-extractor", 3 | "type": "application", 4 | "description": "The Twig Gettext Extractor is Poedit friendly tool which extracts translations from twig templates.", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Saša Stamenković", 9 | "email": "umpirsky@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.1", 14 | "twig/twig": "^2.0", 15 | "twig/extensions": "~1.0", 16 | "symfony/twig-bridge": "^4.0", 17 | "symfony/routing": "^4.0", 18 | "symfony/filesystem": "^4.0", 19 | "symfony/translation": "^4.0", 20 | "symfony/form": "^4.0", 21 | "symfony/asset": "^4.0" 22 | }, 23 | "require-dev": { 24 | "symfony/config": "^4.0", 25 | "phpunit/phpunit": "^6.0" 26 | }, 27 | "autoload": { 28 | "psr-0": { "Twig\\Gettext": "." } 29 | }, 30 | "bin": ["twig-gettext-extractor"], 31 | "config": { 32 | "bin-dir": "bin" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Saša Stamenković 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Twig/Gettext/Extractor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Twig\Gettext; 13 | 14 | use Symfony\Component\Filesystem\Filesystem; 15 | 16 | /** 17 | * Extracts translations from twig templates. 18 | * 19 | * @author Saša Stamenković 20 | */ 21 | class Extractor 22 | { 23 | /** 24 | * @var \Twig_Environment 25 | */ 26 | protected $environment; 27 | 28 | /** 29 | * Gettext parameters. 30 | * 31 | * @var string[] 32 | */ 33 | protected $parameters; 34 | 35 | 36 | private $executable; 37 | 38 | public function __construct(\Twig_Environment $environment) 39 | { 40 | $this->environment = $environment; 41 | $this->reset(); 42 | } 43 | 44 | /** 45 | * @param mixed $executable 46 | */ 47 | public function setExecutable($executable) 48 | { 49 | $this->executable = $executable; 50 | } 51 | 52 | protected function reset() 53 | { 54 | $this->parameters = []; 55 | } 56 | 57 | public function addTemplate($path) 58 | { 59 | $this->environment->loadTemplate($path); 60 | } 61 | 62 | public function addGettextParameter($parameter) 63 | { 64 | $this->parameters[] = $parameter; 65 | } 66 | 67 | public function setGettextParameters(array $parameters) 68 | { 69 | $this->parameters = $parameters; 70 | } 71 | 72 | public function extract() 73 | { 74 | $command = $this->executable ?: 'xgettext'; 75 | $command .= ' ' . implode(' ', $this->parameters); 76 | $command .= ' ' . $this->environment->getCache() . '/*/*.php'; 77 | 78 | $error = 0; 79 | $output = system($command, $error); 80 | if (0 !== $error) { 81 | throw new \RuntimeException(sprintf( 82 | 'Gettext command "%s" failed with error code %s and output: %s', 83 | $command, 84 | $error, 85 | $output 86 | )); 87 | } 88 | 89 | $this->reset(); 90 | } 91 | 92 | public function __destruct() 93 | { 94 | $filesystem = new Filesystem(); 95 | $filesystem->remove($this->environment->getCache()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /twig-gettext-extractor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | /** 14 | * Extracts translations from twig templates. 15 | * 16 | * @author Саша Стаменковић 17 | */ 18 | 19 | if (file_exists($a = __DIR__ . '/../../autoload.php')) { 20 | require_once $a; 21 | } else { 22 | require_once __DIR__ . '/vendor/autoload.php'; 23 | } 24 | 25 | $twig = new Twig_Environment(new Twig\Gettext\Loader\Filesystem(DIRECTORY_SEPARATOR), [ 26 | 'cache' => implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), 'cache', uniqid()]), 27 | 'auto_reload' => true, 28 | ]); 29 | $twig->addExtension(new Twig_Extensions_Extension_I18n()); 30 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\TranslationExtension( 31 | new Symfony\Component\Translation\Translator(null) 32 | )); 33 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\RoutingExtension( 34 | new Twig\Gettext\Routing\Generator\UrlGenerator() 35 | )); 36 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\FormExtension()); 37 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\AssetExtension( 38 | new Symfony\Component\Asset\Packages() 39 | )); 40 | 41 | // You can add more extensions here, or via command line with the --functions and --filter options 42 | 43 | array_shift($_SERVER['argv']); 44 | 45 | $setFunctions = false; 46 | $setFilters = false; 47 | $addTemplate = false; 48 | $setExecutable = false; 49 | 50 | $extractor = new Twig\Gettext\Extractor($twig); 51 | 52 | foreach ($_SERVER['argv'] as $arg) { 53 | if ('--files' === $arg) { 54 | $addTemplate = true; 55 | } else if ($addTemplate) { 56 | $extractor->addTemplate(getcwd() . DIRECTORY_SEPARATOR . $arg); 57 | } else if ('--exec' === $arg) { 58 | $setExecutable = true; 59 | } else if ($setExecutable) { 60 | $extractor->setExecutable($arg); 61 | $setExecutable = false; 62 | } else if ('--functions' === $arg) { 63 | $setFunctions = true; 64 | } else if ($setFunctions) { 65 | foreach (explode(',', $arg) as $functionName) { 66 | $twig->addFunction(new \Twig_SimpleFunction($functionName, true)); 67 | } 68 | $setFunctions = false; 69 | } else if ('--filters' === $arg) { 70 | $setFilters = true; 71 | } else if ($setFilters) { 72 | foreach (explode(',', $arg) as $filterName) { 73 | $twig->addFilter(new \Twig_SimpleFilter($filterName, true)); 74 | } 75 | $setFilters = false; 76 | } else { 77 | $extractor->addGettextParameter($arg); 78 | } 79 | } 80 | 81 | $extractor->extract(); 82 | -------------------------------------------------------------------------------- /Twig/Gettext/Test/ExtractorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Twig\Gettext\Test; 13 | 14 | use Twig\Gettext\Extractor; 15 | use Twig\Gettext\Loader\Filesystem; 16 | use Symfony\Component\Translation\Loader\PoFileLoader; 17 | 18 | /** 19 | * @author Saša Stamenković 20 | */ 21 | class ExtractorTest extends \PHPUnit\Framework\TestCase 22 | { 23 | /** 24 | * @var \Twig_Environment 25 | */ 26 | protected $twig; 27 | 28 | /** 29 | * @var PoFileLoader 30 | */ 31 | protected $loader; 32 | 33 | protected function setUp() 34 | { 35 | $filesystem = new Filesystem('/', __DIR__ . '/Fixtures/twig'); 36 | $filesystem->prependPath(__DIR__ . '/Fixtures/twig'); 37 | $this->twig = new \Twig_Environment($filesystem, [ 38 | 'cache' => '/tmp/cache/' . uniqid(), 39 | 'auto_reload' => true, 40 | ]); 41 | $this->twig->addExtension(new \Twig_Extensions_Extension_I18n()); 42 | 43 | $this->loader = new PoFileLoader(); 44 | } 45 | 46 | /** 47 | * @dataProvider extractDataProvider 48 | */ 49 | public function testExtract(array $templates, array $parameters, array $messages) 50 | { 51 | $extractor = new Extractor($this->twig); 52 | 53 | foreach ($templates as $template) { 54 | $extractor->addTemplate($template); 55 | } 56 | foreach ($parameters as $parameter) { 57 | $extractor->addGettextParameter($parameter); 58 | } 59 | 60 | $extractor->extract(); 61 | 62 | $catalog = $this->loader->load($this->getPotFile(), null); 63 | 64 | foreach ($messages as $message) { 65 | $this->assertTrue( 66 | $catalog->has($message), 67 | sprintf('Message "%s" not found in catalog.', $message) 68 | ); 69 | } 70 | } 71 | 72 | public function extractDataProvider() 73 | { 74 | return [ 75 | [ 76 | [ 77 | '/singular.twig', 78 | '/plural.twig', 79 | ], 80 | $this->getGettextParameters(), 81 | [ 82 | 'Hello %name%!', 83 | 'Hello World!', 84 | 'Hey %name%, I have one apple.', 85 | 'Hey %name%, I have %count% apples.', 86 | ], 87 | ], 88 | ]; 89 | } 90 | 91 | public function testExtractNoTranslations() 92 | { 93 | $extractor = new Extractor($this->twig); 94 | 95 | $extractor->addTemplate('/empty.twig'); 96 | $extractor->setGettextParameters($this->getGettextParameters()); 97 | 98 | $extractor->extract(); 99 | 100 | $catalog = $this->loader->load($this->getPotFile(), null); 101 | 102 | $this->assertEmpty($catalog->all('messages')); 103 | } 104 | 105 | private function getPotFile() 106 | { 107 | return __DIR__ . '/Fixtures/messages.pot'; 108 | } 109 | 110 | private function getGettextParameters() 111 | { 112 | return [ 113 | '--force-po', 114 | '-o', 115 | $this->getPotFile(), 116 | ]; 117 | } 118 | 119 | public function testExtractWithCustomStubs() 120 | { 121 | $extractor = new Extractor($this->twig); 122 | $this->twig->addFunction(new \Twig_SimpleFunction('serverUrl', true)); 123 | $this->twig->addFunction(new \Twig_SimpleFunction('url', true)); 124 | $this->twig->addFunction(new \Twig_SimpleFunction('1', true)); 125 | $extractor->addTemplate(__DIR__ . '/Fixtures/twig/customFunctions.twig'); 126 | $extractor->setGettextParameters($this->getGettextParameters()); 127 | $extractor->extract(); 128 | } 129 | 130 | protected function tearDown() 131 | { 132 | if (file_exists($this->getPotFile())) { 133 | unlink($this->getPotFile()); 134 | } 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

7 | symfony upgrade fixer • 8 | twig gettext extractor • 9 | wisdom • 10 | centipede • 11 | permissions handler • 12 | extraload • 13 | gravatar • 14 | locurro • 15 | country list • 16 | transliterator 17 |

18 | 19 | Twig Gettext Extractor [![Build Status](https://secure.travis-ci.org/umpirsky/Twig-Gettext-Extractor.svg?branch=master)](http://travis-ci.org/umpirsky/Twig-Gettext-Extractor) 20 | ====================== 21 | 22 | The Twig Gettext Extractor is [Poedit](http://www.poedit.net/download.php) 23 | friendly tool which extracts translations from twig templates. 24 | 25 | ## Installation 26 | 27 | ### Manual 28 | 29 | #### Local 30 | 31 | Download the ``twig-gettext-extractor.phar`` file and store it somewhere on your computer. 32 | 33 | #### Global 34 | 35 | You can run these commands to easily access ``twig-gettext-extractor`` from anywhere on 36 | your system: 37 | 38 | ```bash 39 | $ sudo wget https://github.com/umpirsky/Twig-Gettext-Extractor/releases/download/1.2.0/twig-gettext-extractor.phar -O /usr/local/bin/twig-gettext-extractor 40 | $ sudo chmod a+x /usr/local/bin/twig-gettext-extractor 41 | ``` 42 | Then, just run ``twig-gettext-extractor``. 43 | 44 | ### Composer 45 | 46 | #### Local 47 | 48 | ```bash 49 | $ composer require umpirsky/twig-gettext-extractor 50 | ``` 51 | 52 | #### Global 53 | 54 | ```bash 55 | $ composer global require umpirsky/twig-gettext-extractor 56 | ``` 57 | 58 | Make sure you have ``~/.composer/vendor/bin`` in your ``PATH`` and 59 | you're good to go: 60 | 61 | ```bash 62 | $ export PATH="$PATH:$HOME/.composer/vendor/bin" 63 | ``` 64 | Don't forget to add this line in your `.bashrc` file if you want to keep this change after reboot. 65 | 66 | ## Setup 67 | 68 | By default, Poedit does not have the ability to parse Twig templates. 69 | This can be resolved by adding an additional parser (Edit > Preferences > Parsers) 70 | with the following options: 71 | 72 | - Language: `Twig` 73 | - List of extensions: `*.twig` 74 | - Invocation: 75 | - Parser command: `/vendor/bin/twig-gettext-extractor --sort-output --force-po -o %o %C %K -L PHP --files %F` (replace `` with absolute path to your project) 76 | - An item in keyword list: `-k%k` 77 | - An item in input file list: `%f` 78 | - Source code charset: `--from-code=%c` 79 | 80 | 81 | 82 | Now you can update your catalog and Poedit will synchronize it with your twig 83 | templates. 84 | 85 | ## Custom extensions 86 | 87 | Twig-Gettext-Extractor registers some default twig extensions. However, if you are using custom extensions, you need to register them first before you can extract the data. In order to achieve that, copy the binfile into some custom place. A common practice would be: `cp vendor/bin/twig-gettext-extractor bin/twig-gettext-extractor` 88 | 89 | Now you may add your custom extensions [here](https://github.com/umpirsky/Twig-Gettext-Extractor/blob/master/twig-gettext-extractor#L41): 90 | 91 | ```php 92 | $twig->addFunction(new \Twig_SimpleFunction('myCustomExtension', true)); 93 | $twig->addFunction(new \Twig_SimpleFunction('myCustomExtension2', true)); 94 | ``` 95 | 96 | ## Command Line Arguments 97 | 98 | You can also specify custom extensions and filters via command line, by adding the --functions and --filters arguments, e.g.: 99 | ``` 100 | --functions formRow,formElement --filters localizedCurrency 101 | ``` 102 | 103 | You can specify an unlimited amount of comma-separated function and filter names. 104 | 105 | Similarly, you can specify the location of the gettext executable you want to use with --exec 106 | ``` 107 | --exec /usr/local/bin/xgettext 108 | ``` 109 | 110 | 111 | 112 | 113 | --------------------------------------------------------------------------------