├── 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 |
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 [](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 |
--------------------------------------------------------------------------------