diff --git a/README.md b/README.md index 7a6a03b7..10ff4627 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,11 @@ wp i18n Create a POT file for a WordPress plugin or theme. ~~~ -wp i18n make-pot [] [--slug=] [--domain=] [--merge[=]] [--exclude=] +wp i18n make-pot [] [--slug=] [--domain=] [--merge[=]] [--exclude=] [--skip-js] ~~~ +Scans PHP and JavaScript files, as well as theme stylesheets for translatable strings. + **OPTIONS** @@ -57,6 +59,9 @@ wp i18n make-pot [] [--slug=] [--domain=] [- By default, the following files and folders are ignored: node_modules, .git, .svn, .CVS, .hg, vendor. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. + [--skip-js] + Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel. + **EXAMPLES** # Create a POT file for the WordPress plugin/theme in the current directory diff --git a/composer.json b/composer.json index c5e0fa46..aeb0d2e7 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ ] }, "require": { - "gettext/gettext": "dev-master#37882dc3b58c2357fc6ddc204c1f22f7bb2b6601" + "gettext/gettext": "dev-master#37882dc3b58c2357fc6ddc204c1f22f7bb2b6601", + "mck89/peast": "^1.8" }, "require-dev": { "wp-cli/wp-cli": "^1.1.0", diff --git a/features/makepot.feature b/features/makepot.feature index ef93cc51..097ce661 100644 --- a/features/makepot.feature +++ b/features/makepot.feature @@ -66,6 +66,7 @@ Feature: Generate a POT file of a WordPress plugin """ Description of the plugin """ + Scenario: Adds copyright comments When I run `wp scaffold plugin hello-world` @@ -188,12 +189,20 @@ Feature: Generate a POT file of a WordPress plugin __( 'Hello World', 'foo-plugin' ); """ + And a foo-plugin/foo-plugin.js file: + """ + __( 'Hello World', 'foo-plugin' ); + """ When I run `wp i18n make-pot foo-plugin foo-plugin.pot` And the foo-plugin.pot file should contain: """ #: foo-plugin.php:15 """ + And the foo-plugin.pot file should contain: + """ + #: foo-plugin.js:1 + """ Scenario: Uses the current folder as destination path when none is set. When I run `wp scaffold plugin hello-world` @@ -401,14 +410,6 @@ Feature: Generate a POT file of a WordPress plugin """ msgid_plural "__ngettext_noop_plural" """ - And the foo-plugin/foo-plugin.pot file should contain: - """ - msgid "__" - """ - And the foo-plugin/foo-plugin.pot file should contain: - """ - msgid "__" - """ And the foo-plugin/foo-plugin.pot file should not contain: """ msgid "__unsupported_func" @@ -447,7 +448,7 @@ Feature: Generate a POT file of a WordPress plugin /* translators: this should get extracted. */ $foo = __( 'baba', 'foo-plugin' ); - /* translators: boo */ /* translators: this should get extracted too. */ /* some other comment */ $bar = g ( __( 'baba', 'foo-plugin' ) ); + /* translators: boo */ /* translators: this should get extracted too. */ /* some other comment */ $bar = g ( __( 'bubu', 'foo-plugin' ) ); """ When I run `wp i18n make-pot foo-plugin` @@ -997,3 +998,267 @@ Feature: Generate a POT file of a WordPress plugin """ msgid "Foo Plugin" """ + + Scenario: Extracts functions from JavaScript file + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.php file: + """ + {__( 'Hello JSX', 'foo-plugin' )}, + document.getElementById('root') + ); + """ + + When I run `wp i18n make-pot foo-plugin` + Then STDOUT should be: + """ + Plugin file detected. + Success: POT file successfully generated! + """ + And the foo-plugin/foo-plugin.pot file should exist + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "__" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "_x" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgctxt "_x_context" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "_n_single" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid_plural "_n_plural" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "_nx_single" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid_plural "_nx_plural" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgctxt "_nx_context" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "Hello JSX" + """ + And the foo-plugin/foo-plugin.pot file should not contain: + """ + msgid "wrong-domain" + """ + + Scenario: Extract translator comments from JavaScript file + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.php file: + """ + {__( 'Hello JSX', 'foo-plugin' )}, + document.getElementById('root') + ); + """ + + When I run `wp i18n make-pot foo-plugin` + Then STDOUT should be: + """ + Plugin file detected. + Success: POT file successfully generated! + """ + And the foo-plugin/foo-plugin.pot file should exist + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "Foo Plugin" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: Translators 1! + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. Translators: Translators 2! + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + msgid "F j, Y g:i a" + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: localized date and time format, see https://secure.php.net/date + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: let your ears fly! + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. Translators: If there are characters in your language that are not supported by Lato, translate this to 'off'. Do not translate into your own language. + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: this should get extracted. + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: boo + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: this should get extracted too. + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: this is before the multiline call. + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: this is inside the multiline call. + """ + And the foo-plugin/foo-plugin.pot file should contain: + """ + #. translators: this is JSX + """ + + Scenario: Ignores any other text domain in JavaScript file + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.php file: + """ + {__( 'Hello JSX', 'baz' )}, + document.getElementById('root') + ); + """ + + When I run `wp i18n make-pot foo-plugin foo-plugin.pot --domain=bar` + And the foo-plugin.pot file should contain: + """ + msgid "Foo" + """ + And the foo-plugin.pot file should not contain: + """ + msgid "Hello World" + """ + And the foo-plugin.pot file should not contain: + """ + msgid "bar" + """ + And the foo-plugin.pot file should not contain: + """ + msgid "Hello JSX" + """ + + Scenario: Skips JavaScript file altogether + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.php file: + """ + [ 'translators', 'Translators' ], + 'constants' => [], + 'functions' => [ + '__' => 'text_domain', + '_x' => 'text_context_domain', + '_n' => 'single_plural_number_domain', + '_nx' => 'single_plural_number_context_domain', + ], + ]; + + /** + * {@inheritdoc} + */ + public static function fromString( $string, Translations $translations, array $options = [] ) { + $options += static::$options; + + $functions = new JsFunctionsScanner( $string ); + + $functions->enableCommentsExtraction( $options['extractComments'] ); + $functions->saveGettextFunctions( $translations, $options ); + } + + /** + * {@inheritdoc} + */ + public static function fromFile( $file, Translations $translations, array $options = [] ) { + foreach ( static::getFiles( $file ) as $f ) { + // Make sure a relative file path is added as a comment. + $options['file'] = ltrim( str_replace( static::$dir, '', $f ), '/' ); + + $string = file_get_contents( $f ); + + if ( ! $string ) { + WP_CLI::debug( + sprintf( + 'Could not load file %1s', + $f + ) + ); + + continue; + } + + try { + static::fromString( $string, $translations, $options ); + } catch ( PeastException $e ) { + WP_CLI::debug( + sprintf( + 'Could not parse file %1$s: %2$s (line %3$d, column %4$d)', + $options['file'], + $e->getMessage(), + $e->getPosition()->getLine(), + $e->getPosition()->getColumn() + ) + ); + } + } + } + + /** + * Recursively extracts the translations from a directory. + * + * @param string $dir Root path to start the recursive traversal in. + * @param Translations $translations The translations instance to append the new translations. + * @param array $options + */ + public static function fromDirectory( $dir, Translations $translations, array $options = [] ) { + static::$dir = $dir; + + $files = static::getFilesFromDirectory( $dir, isset( $options['exclude'] ) ? $options['exclude'] : [] ); + + if ( ! empty( $files ) ) { + static::fromFile( $files, $translations, $options ); + } + + static::$dir = ''; + } + + /** + * Recursively gets all PHP files within a directory. + * + * @param string $dir A path of a directory. + * @param array $exclude List of files and directories to skip. + * + * @return array File list. + */ + private static function getFilesFromDirectory( $dir, array $exclude = [] ) { + $filtered_files = []; + + $files = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), + function ( $file, $key, $iterator ) use ( $exclude ) { + /** @var DirectoryIterator $file */ + if ( in_array( $file->getBasename(), $exclude, true ) ) { + return false; + } + + // Check for more complex paths, e.g. /some/sub/folder. + foreach( $exclude as $path_or_file ) { + if ( false !== mb_ereg( preg_quote( '/' . $path_or_file ) . '$', $file->getPathname() ) ) { + return false; + } + } + + /** @var RecursiveCallbackFilterIterator $iterator */ + if ( $iterator->hasChildren() ) { + return true; + } + + return ( $file->isFile() && 'js' === $file->getExtension() ); + } + ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $files as $file ) { + /** @var DirectoryIterator $file */ + if ( ! $file->isFile() || 'js' !== $file->getExtension() ) { + continue; + } + + $filtered_files[] = $file->getPathname(); + } + + return $filtered_files; + } +} diff --git a/src/JsFunctionsScanner.php b/src/JsFunctionsScanner.php new file mode 100644 index 00000000..aef4676e --- /dev/null +++ b/src/JsFunctionsScanner.php @@ -0,0 +1,171 @@ +extractComments = $tag; + } + + /** + * Disable comments extraction. + */ + public function disableCommentsExtraction() { + $this->extractComments = false; + } + + /** + * {@inheritdoc} + */ + public function saveGettextFunctions( Translations $translations, array $options ) { + $ast = Peast::latest( $this->code, [ + 'sourceType' => Peast::SOURCE_TYPE_MODULE, + 'comments' => false !== $this->extractComments, + 'jsx' => true, + ] )->parse(); + + $traverser = new Traverser(); + + $all_comments = []; + + /** + * Traverse through JS code to find and extract gettext functions. + * + * Make sure translator comments in front of variable declarations + * and inside nested call expressions are available when parsing the function call. + */ + $traverser->addFunction( function ( $node ) use ( &$translations, $options, &$all_comments ) { + $functions = $options['functions']; + $file = $options['file']; + + /** @var Node\Node $node */ + foreach( $node->getLeadingComments() as $comment ) { + $all_comments[] = $comment; + } + + /** @var Node\CallExpression $node */ + if ( 'CallExpression' !== $node->getType() || 'Identifier' !== $node->getCallee()->getType() ) { + return; + } + + /** @var Node\CallExpression $node */ + foreach ( $node->getArguments() as $argument ) { + // Support nested function calls. + $argument->setLeadingComments( $argument->getLeadingComments() + $node->getLeadingComments() ); + } + + /** @var Node\Identifier $callee */ + $callee = $node->getCallee(); + + if ( ! isset( $functions[ $callee->getName() ] ) ) { + return; + } + + foreach( $callee->getLeadingComments() as $comment ) { + $all_comments[] = $comment; + } + + $domain = $context = $original = $plural = null; + $args = []; + + /** @var Node\Node $argument */ + foreach ( $node->getArguments() as $argument ) { + foreach( $argument->getLeadingComments() as $comment ) { + $all_comments[] = $comment; + } + + if ( 'Identifier' === $argument->getType() ) { + $args[] = ''; // The value doesn't matter as it's unused. + } + + if ( 'Literal' === $argument->getType() ) { + /** @var Node\Literal $argument */ + $args[] = $argument->getValue(); + } + } + + switch ( $functions[ $callee->getName() ] ) { + case 'text_domain': + case 'gettext': + if ( ! isset( $args[1] ) ) { + break; + } + + list( $original, $domain ) = $args; + break; + + case 'text_context_domain': + if ( ! isset( $args[2] ) ) { + break; + } + + list( $original, $context, $domain ) = $args; + break; + + case 'single_plural_number_domain': + if ( ! isset( $args[3] ) ) { + break; + } + + list( $original, $plural, $number, $domain ) = $args; + break; + + case 'single_plural_number_context_domain': + if ( ! isset( $args[4] ) ) { + break; + } + + list( $original, $plural, $number, $context, $domain ) = $args; + break; + } + + if ( (string) $original !== '' && ( $domain === null || $domain === $translations->getDomain() ) ) { + $translation = $translations->insert( $context, $original, $plural ); + $translation->addReference( $file, $node->getLocation()->getStart()->getLine() ); + + /** @var Node\Comment $comment */ + foreach ( $all_comments as $comment ) { + if ( $node->getLocation()->getStart()->getLine() - $comment->getLocation()->getEnd()->getLine() > 1 ) { + continue; + } + + if ( $node->getLocation()->getStart()->getColumn() < $comment->getLocation()->getStart()->getColumn() ) { + continue; + } + + $parsed_comment = ParsedComment::create( $comment->getRawText(), $comment->getLocation()->getStart()->getLine() ); + $prefixes = array_filter( (array) $this->extractComments ); + + if ( $parsed_comment->checkPrefixes( $prefixes ) ) { + $translation->addExtractedComment( $parsed_comment->getComment() ); + } + } + + if ( isset( $parsed_comment ) ) { + $all_comments = []; + } + } + } ); + + $traverser->traverse( $ast ); + } +} diff --git a/src/MakePotCommand.php b/src/MakePotCommand.php index 8d98a2ca..43990d39 100644 --- a/src/MakePotCommand.php +++ b/src/MakePotCommand.php @@ -36,7 +36,7 @@ class MakePotCommand extends WP_CLI_Command { /** * @var array */ - protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor' ]; + protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor', 'Gruntfile.js', 'webpack.config.js' ]; /** * @var string @@ -48,9 +48,16 @@ class MakePotCommand extends WP_CLI_Command { */ protected $main_file_data = []; + /** + * @var bool + */ + protected $skip_js = false; + /** * Create a POT file for a WordPress plugin or theme. * + * Scans PHP and JavaScript files, as well as theme stylesheets for translatable strings. + * * ## OPTIONS * * @@ -74,6 +81,9 @@ class MakePotCommand extends WP_CLI_Command { * By default, the following files and folders are ignored: node_modules, .git, .svn, .CVS, .hg, vendor. * Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. * + * [--skip-js] + * : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel. + * * ## EXAMPLES * * # Create a POT file for the WordPress plugin/theme in the current directory @@ -82,8 +92,9 @@ class MakePotCommand extends WP_CLI_Command { * @when before_wp_load */ public function __invoke( $args, $assoc_args ) { - $this->source = realpath( $args[0] ); - $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) ); + $this->source = realpath( $args[0] ); + $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) ); + $this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js ); if ( ! $this->source || ! is_dir( $this->source ) ) { WP_CLI::error( 'Not a valid source directory!' ); @@ -123,6 +134,8 @@ public function __invoke( $args, $assoc_args ) { } } + WP_CLI::debug( sprintf( 'Destination: %s', $this->destination ), 'make-pot' ); + if ( isset( $assoc_args['exclude'] ) ) { $this->exclude = array_filter( array_merge( $this->exclude, explode( ',', $assoc_args['exclude'] ) ) ); $this->exclude = array_map(function($exclude) { @@ -174,6 +187,7 @@ protected function retrieve_main_file_data() { // Stop when we find a file with a valid Plugin Name header. if ( ! empty( $plugin_data['Plugin Name'] ) ) { WP_CLI::log( 'Plugin file detected.' ); + WP_CLI::debug( sprintf( 'Plugin file: %s', $plugin_file ), 'make-pot' ); $this->main_file_data = $plugin_data; @@ -257,12 +271,6 @@ protected function makepot( $domain ) { $file_data = $this->get_main_file_data(); - // Extract 'Template Name' headers in theme files. - WordPressCodeExtractor::fromDirectory( $this->source, $this->translations, [ - 'wpExtractTemplates' => isset( $file_data['Theme Name'] ), - 'exclude' => $this->exclude, - ] ); - unset( $file_data['Version'], $file_data['License'], $file_data['Domain Path'] ); // Set entries from main file data. @@ -282,6 +290,20 @@ protected function makepot( $domain ) { $this->translations[] = $translation; } + try { + PhpCodeExtractor::fromDirectory( $this->source, $this->translations, [ + // Extract 'Template Name' headers in theme files. + 'wpExtractTemplates' => isset( $file_data['Theme Name'] ), + 'exclude' => $this->exclude, + ] ); + + if ( ! $this->skip_js ) { + JsCodeExtractor::fromDirectory( $this->source, $this->translations ); + } + } catch ( \Exception $e ) { + WP_CLI::error( $e->getMessage() ); + } + foreach( $this->translations as $translation ) { if ( ! $translation->hasExtractedComments() ) { continue; diff --git a/src/WordPressCodeExtractor.php b/src/PhpCodeExtractor.php similarity index 64% rename from src/WordPressCodeExtractor.php rename to src/PhpCodeExtractor.php index 2b4686e8..8b91e02c 100644 --- a/src/WordPressCodeExtractor.php +++ b/src/PhpCodeExtractor.php @@ -8,11 +8,11 @@ use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; -use SplFileInfo; +use DirectoryIterator; use WP_CLI; -class WordPressCodeExtractor extends PhpCode { - protected static $dir = ''; +final class PhpCodeExtractor extends PhpCode { + private static $dir = ''; public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], @@ -50,12 +50,9 @@ class WordPressCodeExtractor extends PhpCode { public static function fromString( $string, Translations $translations, array $options = [] ) { $options += static::$options; - $functions = new WordPressFunctionsScanner( $string ); - - if ( $options['extractComments'] !== false ) { - $functions->enableCommentsExtraction( $options['extractComments'] ); - } + $functions = new PhpFunctionsScanner( $string ); + $functions->enableCommentsExtraction( $options['extractComments'] ); $functions->saveGettextFunctions( $translations, $options ); } @@ -63,13 +60,24 @@ public static function fromString( $string, Translations $translations, array $o * {@inheritdoc} */ public static function fromFile( $file, Translations $translations, array $options = [] ) { - foreach ( self::getFiles( $file ) as $f ) { + foreach ( static::getFiles( $file ) as $f ) { // Make sure a relative file path is added as a comment. $options['file'] = ltrim( str_replace( static::$dir, '', $f ), '/' ); - $string = self::readFile( $f ); + $string = file_get_contents( $f ); + + if ( ! $string ) { + WP_CLI::debug( + sprintf( + 'Could not load file %1s', + $f + ) + ); + + continue; + } - if ( $options['wpExtractTemplates'] ) { + if ( ! empty ( $options['wpExtractTemplates'] ) ) { $headers = MakePotCommand::get_file_data_from_string( $string, [ 'Template Name' => 'Template Name' ] ); if ( ! empty( $headers[ 'Template Name'])) { @@ -96,12 +104,8 @@ public static function fromDirectory( $dir, Translations $translations, array $o $files = static::getFilesFromDirectory( $dir, isset( $options['exclude'] ) ? $options['exclude'] : [] ); - try { - if ( ! empty( $files ) ) { - static::fromFile( $files, $translations, $options ); - } - } catch ( \Exception $e ) { - WP_CLI::error( $e->getMessage() ); + if ( ! empty( $files ) ) { + static::fromFile( $files, $translations, $options ); } static::$dir = ''; @@ -115,45 +119,43 @@ public static function fromDirectory( $dir, Translations $translations, array $o * * @return array File list. */ - protected static function getFilesFromDirectory( $dir, array $exclude = [] ) { + private static function getFilesFromDirectory( $dir, array $exclude = [] ) { $filtered_files = []; - try { - $files = new RecursiveIteratorIterator( - new RecursiveCallbackFilterIterator( - new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), - function ( $file, $key, $iterator ) use ( $exclude ) { - /** @var SplFileInfo $file */ - if ( in_array( $file->getBasename(), $exclude, true ) ) { - return false; - } - - // Check for more complex paths, e.g. /some/sub/folder. - foreach( $exclude as $path_or_file ) { - if ( false !== mb_ereg( preg_quote( '/' . $path_or_file ) . '$', $file->getPathname() ) ) { - return false; - } - } + $files = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), + function ( $file, $key, $iterator ) use ( $exclude ) { + /** @var DirectoryIterator $file */ + if ( in_array( $file->getBasename(), $exclude, true ) ) { + return false; + } - /** @var RecursiveCallbackFilterIterator $iterator */ - if ( $iterator->hasChildren() ) { - return true; + // Check for more complex paths, e.g. /some/sub/folder. + foreach( $exclude as $path_or_file ) { + if ( false !== mb_ereg( preg_quote( '/' . $path_or_file ) . '$', $file->getPathname() ) ) { + return false; } + } - return ( $file->isFile() && 'php' === $file->getExtension() ); + /** @var RecursiveCallbackFilterIterator $iterator */ + if ( $iterator->hasChildren() ) { + return true; } - ), - RecursiveIteratorIterator::CHILD_FIRST - ); - - /** @var SplFileInfo $file */ - foreach ( $files as $file ) { - if ( $file->isFile() ) { - $filtered_files[] = $file->getPathname(); + + return ( $file->isFile() && 'php' === $file->getExtension() ); } + ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $files as $file ) { + /** @var DirectoryIterator $file */ + if ( ! $file->isFile() || 'php' !== $file->getExtension() ) { + continue; } - } catch ( \Exception $e ) { - WP_CLI::error( $e->getMessage() ); + + $filtered_files[] = $file->getPathname(); } return $filtered_files; diff --git a/src/WordPressFunctionsScanner.php b/src/PhpFunctionsScanner.php similarity index 93% rename from src/WordPressFunctionsScanner.php rename to src/PhpFunctionsScanner.php index b98d56dd..16e92f5f 100644 --- a/src/WordPressFunctionsScanner.php +++ b/src/PhpFunctionsScanner.php @@ -3,9 +3,9 @@ namespace WP_CLI\I18n; use Gettext\Translations; -use Gettext\Utils\PhpFunctionsScanner; +use Gettext\Utils\PhpFunctionsScanner as GettextPhpFunctionsScanner; -class WordPressFunctionsScanner extends PhpFunctionsScanner { +final class PhpFunctionsScanner extends GettextPhpFunctionsScanner { /** * {@inheritdoc} */ @@ -77,7 +77,6 @@ public function saveGettextFunctions( Translations $translations, array $options \WP_CLI::error( sprintf( "Internal error: unknown function map '%s' for '%s'.", $functions[ $name ], $name ) ); } - // Todo: Require a domain? if ( (string) $original !== '' && ( $domain === null || $domain === $translations->getDomain() ) ) { $translation = $translations->insert( $context, $original, $plural ); $translation = $translation->addReference( $file, $line );