├── phpstan.neon.dist ├── makefile ├── src ├── Filters │ ├── IFilter.php │ ├── AFilter.php │ ├── LatteFilter.php │ └── PHPFilter.php ├── NetteExtractor.php └── Extractor.php ├── .travis.yml ├── composer.json ├── LICENSE ├── README.md └── gettext-extractor.php /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | paths: 4 | - src 5 | - tests 6 | # - gettext-extractor.php 7 | excludes_analyse: 8 | - tests/integration/data 9 | - tests/unit/data 10 | 11 | includes: 12 | - vendor/phpstan/phpstan-phpunit/extension.neon 13 | - vendor/phpstan/phpstan-phpunit/rules.neon 14 | - vendor/phpstan/phpstan-strict-rules/rules.neon 15 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | help: ## list available targets (this page) 2 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 3 | 4 | check: test phpstan ## run PHPUnit & PHPStan 5 | 6 | test: ## run tests with PHPUnit 7 | ./vendor/bin/phpunit 8 | 9 | phpstan: ## run static analysis with PHPStan 10 | ./vendor/bin/phpstan analyse 11 | 12 | build: ## build .phar file 13 | box compile 14 | 15 | -------------------------------------------------------------------------------- /src/Filters/IFilter.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | public function extract(string $file): array; 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | # Build only commits on master and release tags for the "Build pushed branches" feature. 4 | # This prevents building twice on PRs originating from our repo ("Build pushed pull requests)". 5 | # See: 6 | # - https://github.com/travis-ci/travis-ci/issues/1147 7 | # - https://docs.travis-ci.com/user/pull-requests/#double-builds-on-pull-requests 8 | branches: 9 | only: 10 | - master 11 | - /v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/ 12 | 13 | php: 14 | - '7.3' 15 | - '7.2' 16 | 17 | install: 18 | - composer install --no-interaction --prefer-dist 19 | 20 | script: 21 | - ./vendor/bin/phpunit 22 | - ./vendor/bin/phpstan analyse 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voda/gettext-extractor", 3 | "description": "tool for extracting gettext messages", 4 | "authors": [ 5 | {"name": "Ondřej Vodáček", "email": "ondrej.vodacek@gmail.com"}, 6 | {"name": "Karel Klima"} 7 | ], 8 | "keywords":["gettext", "l10n"], 9 | "license": "BSD-3-Clause", 10 | "config": { 11 | "sort-packages": true 12 | }, 13 | "require": { 14 | "php": ">=7.2.0", 15 | "latte/latte": "^2.5", 16 | "nette/utils": "^3.0", 17 | "nikic/php-parser": "^4.2" 18 | }, 19 | "autoload": { 20 | "psr-4": {"Vodacek\\GettextExtractor\\" : "src"} 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Vodacek\\GettextExtractor\\Tests\\Unit\\": "tests/unit", 25 | "Vodacek\\GettextExtractor\\Tests\\Integration\\": "tests/integration" 26 | } 27 | }, 28 | "bin": [ 29 | "gettext-extractor.php" 30 | ], 31 | "require-dev": { 32 | "phpstan/phpstan": "^0.11.5", 33 | "phpstan/phpstan-phpunit": "^0.11.0", 34 | "phpstan/phpstan-strict-rules": "^0.11.0", 35 | "phpunit/phpunit": "^8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2010, Ondřej Vodáček 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/Filters/AFilter.php: -------------------------------------------------------------------------------- 1 | $singular 33 | ); 34 | if ($plural !== null) { 35 | if ($plural <= 0) { 36 | throw new InvalidArgumentException('Invalid argument type or value given for parameter $plural.'); 37 | } 38 | $function[Extractor::PLURAL] = $plural; 39 | } 40 | if ($context !== null) { 41 | if ($context <= 0) { 42 | throw new InvalidArgumentException('Invalid argument type or value given for parameter $context.'); 43 | } 44 | $function[Extractor::CONTEXT] = $context; 45 | } 46 | $this->functions[$functionName][] = $function; 47 | return $this; 48 | } 49 | 50 | /** 51 | * Excludes a function from the function list 52 | * 53 | * @param string $functionName 54 | * @return self 55 | */ 56 | public function removeFunction(string $functionName): self { 57 | unset($this->functions[$functionName]); 58 | return $this; 59 | } 60 | 61 | /** 62 | * Excludes all functions from the function list 63 | * 64 | * @return self 65 | */ 66 | public function removeAllFunctions(): self { 67 | $this->functions = array(); 68 | return $this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GettextExtractor 2 | ================ 3 | Tool for extracting gettext phrases from PHP files and Latte templates. Output is generated as a .pot file. 4 | 5 | [![Build Status](https://travis-ci.org/voda/gettext-extractor.svg?branch=master)](https://travis-ci.org/voda/gettext-extractor) 6 | [![Latest Stable Version](https://poser.pugx.org/voda/gettext-extractor/v/stable)](https://packagist.org/packages/voda/gettext-extractor) 7 | [![Total Downloads](https://poser.pugx.org/voda/gettext-extractor/downloads)](https://packagist.org/packages/voda/gettext-extractor) 8 | [![License](https://poser.pugx.org/voda/gettext-extractor/license)](https://packagist.org/packages/voda/gettext-extractor) 9 | 10 | Installation 11 | ------------ 12 | To install gettext-extractor install it with [composer](https://getcomposer.org/): 13 | `$ composer require --dev voda/gettext-extractor` 14 | 15 | Alternatively you can download a standalone PHAR file from [releases page](https://github.com/voda/gettext-extractor/releases). 16 | 17 | Usage 18 | ----- 19 | `./vendor/bin/gettext-extractor [options]` 20 | 21 | Options: 22 | -h display this help and exit 23 | -oFILE output file, default output is stdout 24 | -lFILE log file, default is stderr 25 | -fFILE file to extract, can be specified several times 26 | -kFUNCTION add FUNCTION to filters, format is: 27 | FILTER:FUNCTION_NAME:SINGULAR,PLURAL,CONTEXT 28 | default FILTERs are PHP and Latte 29 | for SINGULAR, PLURAL and CONTEXT '0' means not set 30 | can be specified several times 31 | -mKEY:VALUE set meta header 32 | 33 | e.g.: `./vendor/bin/gettext-extractor -o outup/file.pot -f files/to/extract/` 34 | 35 | Supported file types 36 | -------------------- 37 | * .php 38 | * .latte (Nette Latte templates) 39 | 40 | License 41 | ------- 42 | GettextExtractor is licensed under the New BSD License. 43 | 44 | Based on code from [Karel Klíma](https://github.com/karelklima/gettext-extractor). 45 | -------------------------------------------------------------------------------- /src/NetteExtractor.php: -------------------------------------------------------------------------------- 1 | removeAllFilters(); 21 | 22 | // Set basic filters 23 | $this->setFilter('php', 'PHP') 24 | ->setFilter('phtml', 'PHP') 25 | ->setFilter('phtml', 'Latte') 26 | ->setFilter('latte', 'PHP') 27 | ->setFilter('latte', 'Latte'); 28 | 29 | $this->addFilter('Latte', new Filters\LatteFilter()); 30 | 31 | $phpFilter = $this->getFilter('PHP'); 32 | assert($phpFilter instanceof PHPFilter); 33 | 34 | $phpFilter->addFunction('translate'); 35 | 36 | $latteFilter = $this->getFilter('Latte'); 37 | assert($latteFilter instanceof LatteFilter); 38 | 39 | $latteFilter->addFunction('!_') 40 | ->addFunction('_'); 41 | } 42 | 43 | /** 44 | * Optional setup of Forms translations 45 | * 46 | * @return self 47 | */ 48 | public function setupForms(): self { 49 | $php = $this->getFilter('PHP'); 50 | assert($php instanceof PHPFilter); 51 | 52 | $php->addFunction('setText') 53 | ->addFunction('setEmptyValue') 54 | ->addFunction('setValue') 55 | ->addFunction('addButton', 2) 56 | ->addFunction('addCheckbox', 2) 57 | ->addFunction('addError') 58 | ->addFunction('addFile', 2) // Nette 0.9 59 | ->addFunction('addGroup') 60 | ->addFunction('addImage', 3) 61 | ->addFunction('addMultiSelect', 2) 62 | ->addFunction('addMultiSelect', 3) 63 | ->addFunction('addPassword', 2) 64 | ->addFunction('addRadioList', 2) 65 | ->addFunction('addRadioList', 3) 66 | ->addFunction('addRule', 2) 67 | ->addFunction('addSelect', 2) 68 | ->addFunction('addSelect', 3) 69 | ->addFunction('addSubmit', 2) 70 | ->addFunction('addText', 2) 71 | ->addFunction('addTextArea', 2) 72 | ->addFunction('addUpload', 2) // Nette 2.0 73 | ->addFunction('setRequired') 74 | ->addFunction('setDefaultValue') 75 | ->addFunction('skipFirst') // Nette 0.9 76 | ->addFunction('setPrompt') // Nette 2.0 77 | ->addFunction('addProtection'); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Optional setup of DataGrid component translations 84 | * 85 | * @return self 86 | */ 87 | public function setupDataGrid(): self { 88 | $php = $this->getFilter('PHP'); 89 | assert($php instanceof PHPFilter); 90 | 91 | $php->addFunction('addColumn', 2) 92 | ->addFunction('addNumericColumn', 2) 93 | ->addFunction('addDateColumn', 2) 94 | ->addFunction('addCheckboxColumn', 2) 95 | ->addFunction('addImageColumn', 2) 96 | ->addFunction('addPositionColumn', 2) 97 | ->addFunction('addActionColumn') 98 | ->addFunction('addAction'); 99 | 100 | return $this; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /gettext-extractor.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | $filter, 77 | 'function' => $function, 78 | 'singular' => isset($params[0]) ? (int) $params[0] : null, 79 | 'plural' => isset($params[1]) ? (int) $params[1] : null, 80 | 'context' => isset($params[2]) ? (int) $params[2] : null 81 | ); 82 | } 83 | } 84 | if (isset($options['m'])) { 85 | if (is_string($options['m'])) { 86 | $options['m'] = array($options['m']); 87 | } 88 | $key = $value = null; 89 | foreach ($options['m'] as $m) { 90 | list($key, $value) = explode(':', $m, 2); 91 | $meta[$key] = $value; 92 | } 93 | } 94 | 95 | $extractor = new Vodacek\GettextExtractor\NetteExtractor($log); 96 | $extractor->setupForms()->setupDataGrid(); 97 | if ($keywords !== null) { 98 | foreach ($keywords as $value) { 99 | $extractor->getFilter($value['filter']) 100 | ->addFunction($value['function'], $value['singular'], $value['plural'], $value['context']); 101 | } 102 | } 103 | if ($meta) { 104 | foreach ($meta as $key => $value) { 105 | $extractor->setMeta($key, $value); 106 | } 107 | } 108 | $extractor->scan($options['f']); 109 | $extractor->save($output); 110 | -------------------------------------------------------------------------------- /src/Filters/LatteFilter.php: -------------------------------------------------------------------------------- 1 | addFunction('_'); 23 | $this->addFunction('!_'); 24 | $this->addFunction('_n', 1, 2); 25 | $this->addFunction('!_n', 1, 2); 26 | $this->addFunction('_p', 2, null, 1); 27 | $this->addFunction('!_p', 2, null, 1); 28 | $this->addFunction('_np', 2, 3, 1); 29 | $this->addFunction('!_np', 2, 3, 1); 30 | } 31 | 32 | public function extract(string $file): array { 33 | $data = array(); 34 | 35 | $latteParser = new Latte\Parser(); 36 | $tokens = $latteParser->parse(FileSystem::read($file)); 37 | 38 | $functions = array_keys($this->functions); 39 | usort($functions, static function(string $a, string $b) { 40 | return strlen($b) <=> strlen($a); 41 | }); 42 | 43 | $phpParser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7); 44 | foreach ($tokens as $token) { 45 | if ($token->type !== Latte\Token::MACRO_TAG) { 46 | continue; 47 | } 48 | 49 | $name = $this->findMacroName($token->text, $functions); 50 | if ($name === null) { 51 | continue; 52 | } 53 | $value = $this->trimMacroValue($name, $token->value); 54 | $stmts = $phpParser->parse("expr instanceof FuncCall) { 60 | foreach ($this->functions[$name] as $definition) { 61 | $message = $this->processFunction($definition, $stmts[0]->expr); 62 | if ($message !== []) { 63 | $message[Extractor::LINE] = $token->line; 64 | $data[] = $message; 65 | } 66 | } 67 | } 68 | } 69 | return $data; 70 | } 71 | 72 | private function processFunction(array $definition, FuncCall $node): array { 73 | $message = []; 74 | foreach ($definition as $type => $position) { 75 | if (!isset($node->args[$position - 1])) { 76 | return []; 77 | } 78 | $arg = $node->args[$position - 1]->value; 79 | if ($arg instanceof String_) { 80 | $message[$type] = $arg->value; 81 | } else { 82 | return []; 83 | } 84 | } 85 | return $message; 86 | } 87 | 88 | private function findMacroName(string $text, array $functions): ?string { 89 | foreach ($functions as $function) { 90 | if (strpos($text, '{'.$function) === 0) { 91 | return $function; 92 | } 93 | } 94 | return null; 95 | } 96 | 97 | private function trimMacroValue(string $name, string $value): string { 98 | if (strpos($name, '!') === 0) { 99 | // exclamation mark is never removed 100 | return trim(substr($value, strlen($name))); 101 | } 102 | 103 | if (strpos($name, '_') === 0) { 104 | // only underscore is removed 105 | $offset = strlen(ltrim($name, '_')); 106 | return substr($value, $offset); 107 | } 108 | 109 | return $value; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Filters/PHPFilter.php: -------------------------------------------------------------------------------- 1 | addFunction('gettext', 1); 30 | $this->addFunction('_', 1); 31 | $this->addFunction('ngettext', 1, 2); 32 | $this->addFunction('_n', 1, 2); 33 | $this->addFunction('pgettext', 2, null, 1); 34 | $this->addFunction('_p', 2, null, 1); 35 | $this->addFunction('npgettext', 2, 3, 1); 36 | $this->addFunction('_np', 2, 3, 1); 37 | } 38 | 39 | public function extract(string $file): array { 40 | $this->data = array(); 41 | $parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7); 42 | $stmts = $parser->parse(FileSystem::read($file)); 43 | if ($stmts === null) { 44 | return []; 45 | } 46 | $traverser = new PhpParser\NodeTraverser(); 47 | $traverser->addVisitor($this); 48 | $traverser->traverse($stmts); 49 | $data = $this->data; 50 | $this->data = []; 51 | return $data; 52 | } 53 | 54 | public function enterNode(Node $node) { 55 | $name = null; 56 | $args = []; 57 | if (($node instanceof MethodCall || $node instanceof StaticCall) && $node->name instanceof Identifier) { 58 | $name = $node->name->name; 59 | $args = $node->args; 60 | } elseif ($node instanceof FuncCall && $node->name instanceof Name) { 61 | $parts = $node->name->parts; 62 | $name = array_pop($parts); 63 | $args = $node->args; 64 | } else { 65 | return null; 66 | } 67 | if (!isset($this->functions[$name])) { 68 | return null; 69 | } 70 | foreach ($this->functions[$name] as $definition) { 71 | $this->processFunction($definition, $node, $args); 72 | } 73 | } 74 | 75 | /** 76 | * @param array $definition 77 | * @param Node $node 78 | * @param Arg[] $args 79 | */ 80 | private function processFunction(array $definition, Node $node, array $args): void { 81 | $message = array( 82 | Extractor::LINE => $node->getLine() 83 | ); 84 | foreach ($definition as $type => $position) { 85 | if (!isset($args[$position - 1])) { 86 | return; 87 | } 88 | $arg = $args[$position - 1]->value; 89 | if ($arg instanceof String_) { 90 | $message[$type] = $arg->value; 91 | } elseif ($arg instanceof Array_) { 92 | foreach ($arg->items as $item) { 93 | if ($item->value instanceof String_) { 94 | $message[$type][] = $item->value->value; 95 | } 96 | } 97 | if (count($message) === 1) { // line only 98 | return; 99 | } 100 | } elseif ($arg instanceof Node\Expr\BinaryOp\Concat) { 101 | $message[$type] = $this->processConcatenatedString($arg); 102 | } else { 103 | return; 104 | } 105 | } 106 | if (is_array($message[Extractor::SINGULAR])) { 107 | foreach ($message[Extractor::SINGULAR] as $value) { 108 | $tmp = $message; 109 | $tmp[Extractor::SINGULAR] = $value; 110 | $this->data[] = $tmp; 111 | } 112 | } else { 113 | $this->data[] = $message; 114 | } 115 | } 116 | 117 | private function processConcatenatedString(Node\Expr\BinaryOp\Concat $arg): string 118 | { 119 | $result = ''; 120 | 121 | if ($arg->left instanceof Node\Expr\BinaryOp\Concat) { 122 | $result .= $this->processConcatenatedString($arg->left); 123 | } elseif ($arg->left instanceof String_) { 124 | $result .= $arg->left->value; 125 | } 126 | 127 | if ($arg->right instanceof String_) { 128 | $result .= $arg->right->value; 129 | } 130 | 131 | return $result; 132 | } 133 | 134 | /* PhpParser\NodeVisitor: dont need these *******************************/ 135 | 136 | public function afterTraverse(array $nodes) { 137 | } 138 | 139 | public function beforeTraverse(array $nodes) { 140 | } 141 | 142 | public function leaveNode(Node $node) { 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Extractor.php: -------------------------------------------------------------------------------- 1 | array('PHP') 36 | ); 37 | 38 | /** @var array */ 39 | protected $filterStore = array(); 40 | 41 | /** @var array */ 42 | protected $comments = array( 43 | 'Gettext keys exported by GettextExtractor' 44 | ); 45 | 46 | /** @var array */ 47 | protected $meta = array( 48 | 'POT-Creation-Date' => '', 49 | 'PO-Revision-Date' => 'YEAR-MO-DA HO:MI+ZONE', 50 | 'Last-Translator' => 'FULL NAME ', 51 | 'Language-Team' => 'LANGUAGE ', 52 | 'MIME-Version' => '1.0', 53 | 'Content-Type' => 'text/plain; charset=UTF-8', 54 | 'Content-Transfer-Encoding' => '8bit', 55 | 'Plural-Forms' => 'nplurals=INTEGER; plural=EXPRESSION;' 56 | ); 57 | 58 | /** @var array */ 59 | protected $data = array(); 60 | 61 | public function __construct(string $logFile = 'php://stderr') { 62 | $this->logFile = $logFile; 63 | $this->addFilter('PHP', new Filters\PHPFilter()); 64 | $this->setMeta('POT-Creation-Date', date('c')); 65 | } 66 | 67 | /** 68 | * Writes messages into log or dumps them on screen 69 | * 70 | * @param string $message 71 | */ 72 | public function log(string $message): void { 73 | if ($this->logFile !== '') { 74 | file_put_contents($this->logFile, "$message\n", FILE_APPEND); 75 | } 76 | } 77 | 78 | protected function throwException(string $message): void { 79 | $message = $message ?: 'Something unexpected occured. See GettextExtractor log for details'; 80 | $this->log($message); 81 | throw new RuntimeException($message); 82 | } 83 | 84 | /** 85 | * Scans given files or directories and extracts gettext keys from the content 86 | * 87 | * @param string|string[] $resource 88 | * @return self 89 | */ 90 | public function scan($resource): self { 91 | $this->inputFiles = array(); 92 | if (!is_array($resource)) { 93 | $resource = array($resource); 94 | } 95 | foreach ($resource as $item) { 96 | $this->log("Scanning '$item'"); 97 | $this->_scan($item); 98 | } 99 | $this->_extract($this->inputFiles); 100 | return $this; 101 | } 102 | 103 | /** 104 | * Scans given files or directories (recursively) 105 | * 106 | * @param string $resource File or directory 107 | */ 108 | private function _scan(string $resource): void { 109 | if (is_file($resource)) { 110 | $this->inputFiles[] = $resource; 111 | } elseif (is_dir($resource)) { 112 | $iterator = new RecursiveIteratorIterator( 113 | new RecursiveDirectoryIterator($resource, RecursiveDirectoryIterator::SKIP_DOTS) 114 | ); 115 | foreach ($iterator as $file) { 116 | $this->inputFiles[] = $file->getPathName(); 117 | } 118 | } else { 119 | $this->throwException("Resource '$resource' is not a directory or file"); 120 | } 121 | } 122 | 123 | /** 124 | * Extracts gettext keys from input files 125 | * 126 | * @param string[] $inputFiles 127 | * @return array 128 | */ 129 | private function _extract(array $inputFiles): array { 130 | $inputFiles = array_unique($inputFiles); 131 | sort($inputFiles); 132 | foreach ($inputFiles as $inputFile) { 133 | if (!file_exists($inputFile)) { 134 | $this->throwException('ERROR: Invalid input file specified: '.$inputFile); 135 | } 136 | if (!is_readable($inputFile)) { 137 | $this->throwException('ERROR: Input file is not readable: '.$inputFile); 138 | } 139 | 140 | $this->log('Extracting data from file '.$inputFile); 141 | 142 | $fileExtension = pathinfo($inputFile, PATHINFO_EXTENSION); 143 | if (isset($this->filters[$fileExtension])) { 144 | $this->log('Processing file '.$inputFile); 145 | 146 | foreach ($this->filters[$fileExtension] as $filterName) { 147 | $filter = $this->getFilter($filterName); 148 | $filterData = $filter->extract($inputFile); 149 | $this->log(' Filter '.$filterName.' applied'); 150 | $this->addMessages($filterData, $inputFile); 151 | } 152 | } 153 | } 154 | return $this->data; 155 | } 156 | 157 | public function getFilter(string $filterName): IFilter { 158 | if (isset($this->filterStore[$filterName])) { 159 | return $this->filterStore[$filterName]; 160 | } 161 | $this->throwException("ERROR: Filter '$filterName' not found."); 162 | } 163 | 164 | /** 165 | * Assigns a filter to an extension 166 | * 167 | * @param string $extension 168 | * @param string $filterName 169 | * @return self 170 | */ 171 | public function setFilter(string $extension, string $filterName): self { 172 | if (!isset($this->filters[$extension]) || !in_array($filterName, $this->filters[$extension], true)) { 173 | $this->filters[$extension][] = $filterName; 174 | } 175 | return $this; 176 | } 177 | 178 | /** 179 | * Add a filter object 180 | * 181 | * @param string $filterName 182 | * @param IFilter $filter 183 | * @return self 184 | */ 185 | public function addFilter(string $filterName, IFilter $filter): self { 186 | $this->filterStore[$filterName] = $filter; 187 | return $this; 188 | } 189 | 190 | /** 191 | * Removes all filter settings in case we want to define a brand new one 192 | * 193 | * @return self 194 | */ 195 | public function removeAllFilters(): self { 196 | $this->filters = array(); 197 | return $this; 198 | } 199 | 200 | /** 201 | * Adds a comment to the top of the output file 202 | * 203 | * @param string $value 204 | * @return self 205 | */ 206 | public function addComment(string $value): self { 207 | $this->comments[] = $value; 208 | return $this; 209 | } 210 | 211 | /** 212 | * Gets a value of a meta key 213 | * 214 | * @param string $key 215 | * @return string|null 216 | */ 217 | public function getMeta(string $key): ?string { 218 | return $this->meta[$key] ?? null; 219 | } 220 | 221 | /** 222 | * Sets a value of a meta key 223 | * 224 | * @param string $key 225 | * @param string $value 226 | * @return self 227 | */ 228 | public function setMeta(string $key, string $value): self { 229 | $this->meta[$key] = $value; 230 | return $this; 231 | } 232 | 233 | /** 234 | * Saves extracted data into gettext file 235 | * 236 | * @param string $outputFile 237 | * @param array $data 238 | * @return self 239 | */ 240 | public function save(string $outputFile, array $data = null): self { 241 | FileSystem::write($outputFile, $this->formatData($data ?: $this->data)); 242 | return $this; 243 | } 244 | 245 | /** 246 | * Formats fetched data to gettext syntax 247 | * 248 | * @param array $data 249 | * @return string 250 | */ 251 | private function formatData(array $data): string { 252 | $output = array(); 253 | foreach ($this->comments as $comment) { 254 | $output[] = '# '.$comment; 255 | } 256 | $output[] = '#, fuzzy'; 257 | $output[] = 'msgid ""'; 258 | $output[] = 'msgstr ""'; 259 | foreach ($this->meta as $key => $value) { 260 | $output[] = '"'.$key.': '.$value.'\n"'; 261 | } 262 | $output[] = ''; 263 | 264 | foreach ($data as $message) { 265 | foreach ($message['files'] as $file) { 266 | $output[] = '#: '.$file[self::FILE].':'.$file[self::LINE]; 267 | } 268 | if (isset($message[self::CONTEXT])) { 269 | $output[] = $this->formatMessage($message[self::CONTEXT], 'msgctxt'); 270 | } 271 | $output[] = $this->formatMessage($message[self::SINGULAR], 'msgid'); 272 | if (isset($message[self::PLURAL])) { 273 | $output[] = $this->formatMessage($message[self::PLURAL], 'msgid_plural'); 274 | $output[] = 'msgstr[0] ""'; 275 | $output[] = 'msgstr[1] ""'; 276 | } else { 277 | $output[] = 'msgstr ""'; 278 | } 279 | 280 | $output[] = ''; 281 | } 282 | 283 | return implode("\n", $output); 284 | } 285 | 286 | private function addMessages(array $messages, string $file): void { 287 | foreach ($messages as $message) { 288 | $key = ''; 289 | if (isset($message[self::CONTEXT])) { 290 | $key .= $message[self::CONTEXT]; 291 | } 292 | $key .= chr(4); 293 | $key .= $message[self::SINGULAR]; 294 | $key .= chr(4); 295 | if (isset($message[self::PLURAL])) { 296 | $key .= $message[self::PLURAL]; 297 | } 298 | if ($key === chr(4).chr(4)) { 299 | continue; 300 | } 301 | $line = $message[self::LINE]; 302 | if (!isset($this->data[$key])) { 303 | unset($message[self::LINE]); 304 | $this->data[$key] = $message; 305 | $this->data[$key]['files'] = array(); 306 | } 307 | $this->data[$key]['files'][] = array( 308 | self::FILE => $file, 309 | self::LINE => $line 310 | ); 311 | } 312 | } 313 | 314 | private function formatMessage(string $message, string $prefix = null): string { 315 | $message = addcslashes($message, self::ESCAPE_CHARS); 316 | $message = '"' . str_replace("\n", "\\n\"\n\"", $message) . '"'; 317 | return ($prefix !== null ? $prefix.' ' : '') . $message; 318 | } 319 | } 320 | --------------------------------------------------------------------------------