diff --git a/features/makejson.feature b/features/makejson.feature index f5314ef5..007e9b8d 100644 --- a/features/makejson.feature +++ b/features/makejson.feature @@ -1089,3 +1089,51 @@ Feature: Split PO files into JSON files. """ "source":"app.min.admin.js" """ + + Scenario: Prints validation warnings for strings with placeholders + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin-de_DE.po file: + """ + # Copyright (C) 2018 Foo Plugin + # This file is distributed under the same license as the Foo Plugin package. + msgid "" + msgstr "" + "Project-Id-Version: Foo Plugin\n" + "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: de_DE\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "POT-Creation-Date: 2018-05-02T22:06:24+00:00\n" + "PO-Revision-Date: 2018-05-02T22:06:24+00:00\n" + "X-Domain: foo-plugin\n" + "Plural-Forms: nplurals=2; plural=(n != 1);\n" + + #: foo-plugin.js:10 + msgid "Hello, %s" + msgstr "Hallo, %s" + + #: foo-plugin.js:15 + msgid "One Comment" + msgid_plural "%s Comments" + msgstr[0] "Ein Kommentar" + msgstr[1] "%s Kommentare" + """ + + When I try `wp i18n make-json foo-plugin` + Then STDOUT should contain: + """ + Success: Created 1 file. + """ + And STDERR should contain: + """ + Warning: The string "Hello, %s" contains placeholders but has no "translators:" comment to clarify their meaning. (foo-plugin.js:10) + """ + And STDERR should contain: + """ + Warning: Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals (foo-plugin.js:15) + """ + And the return code should be 0 + diff --git a/features/makemo.feature b/features/makemo.feature index ae0e2585..9f96a98c 100644 --- a/features/makemo.feature +++ b/features/makemo.feature @@ -254,3 +254,51 @@ Feature: Generate MO files from PO files JS Only Translation """ + Scenario: Prints validation warnings for strings with placeholders + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin-de_DE.po file: + """ + # Copyright (C) 2018 Foo Plugin + # This file is distributed under the same license as the Foo Plugin package. + msgid "" + msgstr "" + "Project-Id-Version: Foo Plugin\n" + "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: de_DE\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "POT-Creation-Date: 2018-05-02T22:06:24+00:00\n" + "PO-Revision-Date: 2018-05-02T22:06:24+00:00\n" + "X-Domain: foo-plugin\n" + "Plural-Forms: nplurals=2; plural=(n != 1);\n" + + #: foo-plugin.php:10 + msgid "Hello, %s" + msgstr "Hallo, %s" + + #: foo-plugin.php:15 + msgid "One Comment" + msgid_plural "%s Comments" + msgstr[0] "Ein Kommentar" + msgstr[1] "%s Kommentare" + """ + + When I try `wp i18n make-mo foo-plugin` + Then STDOUT should contain: + """ + Success: Created 1 file. + """ + And STDERR should contain: + """ + Warning: The string "Hello, %s" contains placeholders but has no "translators:" comment to clarify their meaning. (foo-plugin.php:10) + """ + And STDERR should contain: + """ + Warning: Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals (foo-plugin.php:15) + """ + And the return code should be 0 + And the foo-plugin/foo-plugin-de_DE.mo file should exist + diff --git a/features/makephp.feature b/features/makephp.feature index 66f12711..7e256411 100644 --- a/features/makephp.feature +++ b/features/makephp.feature @@ -144,13 +144,16 @@ Feature: Generate PHP files from PO files msgstr "[%s] Export personenbezogener Daten" """ - When I run `wp i18n make-php foo-plugin` + When I try `wp i18n make-php foo-plugin` Then STDOUT should contain: """ Success: Created 1 file. """ And the return code should be 0 - And STDERR should be empty + And STDERR should contain: + """ + Warning: + """ And the foo-plugin/foo-plugin-de_DE.l10n.php file should contain: """ \n" + "Language-Team: LANGUAGE \n" + "Language: de_DE\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "POT-Creation-Date: 2018-05-02T22:06:24+00:00\n" + "PO-Revision-Date: 2018-05-02T22:06:24+00:00\n" + "X-Domain: foo-plugin\n" + "Plural-Forms: nplurals=2; plural=(n != 1);\n" + + #: foo-plugin.php:10 + msgid "Hello, %s" + msgstr "Hallo, %s" + + #: foo-plugin.php:15 + msgid "One Comment" + msgid_plural "%s Comments" + msgstr[0] "Ein Kommentar" + msgstr[1] "%s Kommentare" + """ + + When I try `wp i18n make-php foo-plugin` + Then STDOUT should contain: + """ + Success: Created 1 file. + """ + And STDERR should contain: + """ + Warning: The string "Hello, %s" contains placeholders but has no "translators:" comment to clarify their meaning. (foo-plugin.php:10) + """ + And STDERR should contain: + """ + Warning: Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals (foo-plugin.php:15) + """ + And the return code should be 0 + And the foo-plugin/foo-plugin-de_DE.l10n.php file should exist + diff --git a/src/MakeJsonCommand.php b/src/MakeJsonCommand.php index b52c9a29..c4f6b74e 100644 --- a/src/MakeJsonCommand.php +++ b/src/MakeJsonCommand.php @@ -13,6 +13,8 @@ use SplFileInfo; class MakeJsonCommand extends WP_CLI_Command { + use StringAuditTrait; + /** * Options passed to json_encode(). * @@ -220,6 +222,9 @@ protected function make_json( $source_file, $destination, $map, $domain, $extens PoExtractor::fromFile( $source_file, $translations ); + // Audit strings for potential issues. + $this->perform_string_audit( $translations ); + $base_file_name = basename( $source_file, '.po' ); $domain = ( ! empty( $domain ) ) ? $domain : $translations->getDomain(); diff --git a/src/MakeMoCommand.php b/src/MakeMoCommand.php index 89bc9865..9c2b56a6 100644 --- a/src/MakeMoCommand.php +++ b/src/MakeMoCommand.php @@ -13,6 +13,7 @@ class MakeMoCommand extends WP_CLI_Command { use JsStringFilterTrait; + use StringAuditTrait; /** * Create MO files from PO files. @@ -92,6 +93,9 @@ public function __invoke( $args, $assoc_args ) { $translations = Translations::fromPoFile( $file->getPathname() ); + // Audit strings for potential issues. + $this->perform_string_audit( $translations ); + // Remove JS-only strings from MO files to keep them small. $this->remove_js_only_strings( $translations ); diff --git a/src/MakePhpCommand.php b/src/MakePhpCommand.php index adecee2c..83c4a99d 100644 --- a/src/MakePhpCommand.php +++ b/src/MakePhpCommand.php @@ -13,6 +13,7 @@ class MakePhpCommand extends WP_CLI_Command { use JsStringFilterTrait; + use StringAuditTrait; /** * Create PHP files from PO files. @@ -86,6 +87,9 @@ public function __invoke( $args, $assoc_args ) { $translations = Translations::fromPoFile( $file->getPathname() ); + // Audit strings for potential issues. + $this->perform_string_audit( $translations ); + // Remove JS-only strings from PHP files to keep them small. $this->remove_js_only_strings( $translations ); diff --git a/src/MakePotCommand.php b/src/MakePotCommand.php index 3d14245d..884c5ab3 100644 --- a/src/MakePotCommand.php +++ b/src/MakePotCommand.php @@ -14,6 +14,8 @@ use IteratorIterator; class MakePotCommand extends WP_CLI_Command { + use StringAuditTrait; + /** * @var string */ @@ -780,133 +782,8 @@ protected function extract_strings() { * @param Translations $translations Translations object. */ protected function audit_strings( $translations ) { - foreach ( $translations as $translation ) { - /** @var Translation $translation */ - - $references = $translation->getReferences(); - - // File headers don't have any file references. - $location = $translation->hasReferences() ? '(' . implode( ':', $references[0] ) . ')' : ''; - - // Check 1: Flag strings with placeholders that should have translator comments. - if ( - ! $translation->hasExtractedComments() && - preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders ) >= 1 - ) { - $message = sprintf( - 'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s', - $translation->getOriginal(), - $location - ); - WP_CLI::warning( $message ); - } - - // Check 2: Flag strings with different translator comments. - if ( $translation->hasExtractedComments() ) { - $comments = $translation->getExtractedComments(); - - // Remove plugin header information from comments. - $comments = array_filter( - $comments, - function ( $comment ) { - /** @var ParsedComment|string $comment */ - /** @var string $file_header */ - foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) { - if ( 0 === strpos( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $file_header ) ) { - return null; - } - } - - return $comment; - } - ); - - $unique_comments = array(); - - // Remove duplicate comments. - $comments = array_filter( - $comments, - function ( $comment ) use ( &$unique_comments ) { - /** @var ParsedComment|string $comment */ - if ( in_array( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $unique_comments, true ) ) { - return null; - } - - $unique_comments[] = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ); - - return $comment; - } - ); - - $comments_count = count( $comments ); - - if ( $comments_count > 1 ) { - $message = sprintf( - "The string \"%1\$s\" has %2\$d different translator comments. %3\$s\n%4\$s", - $translation->getOriginal(), - $comments_count, - $location, - implode( "\n", $unique_comments ) - ); - WP_CLI::warning( $message ); - } - } - - $non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) ); - $non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content ); - - // Check 3: Flag empty strings without any translatable content. - if ( '' === $non_placeholder_content ) { - $message = sprintf( - 'Found string without translatable content. %s', - $location - ); - WP_CLI::warning( $message ); - } - - // Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s). - $unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches ); - $unordered_matches = $unordered_matches[0]; - - if ( $unordered_matches_count >= 2 ) { - $message = sprintf( - 'Multiple placeholders should be ordered. %s', - $location - ); - WP_CLI::warning( $message ); - } - - if ( $translation->hasPlural() ) { - preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders ); - $single_placeholders = $single_placeholders[0]; - - preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders ); - $plural_placeholders = $plural_placeholders[0]; - - // see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals - if ( count( $single_placeholders ) < count( $plural_placeholders ) ) { - // Check 5: Flag things like _n( 'One comment', '%s Comments' ) - $message = sprintf( - 'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s', - $location - ); - WP_CLI::warning( $message ); - } else { - // Reordering is fine, but mismatched placeholders is probably wrong. - sort( $single_placeholders ); - sort( $plural_placeholders ); - - // Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' ) - if ( $single_placeholders !== $plural_placeholders ) { - $message = sprintf( - 'Mismatched placeholders for singular and plural string. %s', - $location - ); - WP_CLI::warning( $message ); - } - } - } - } + // Call the trait's perform_string_audit method with the appropriate file headers. + $this->perform_string_audit( $translations, $this->get_file_headers( $this->project_type ) ); } /** diff --git a/src/StringAuditTrait.php b/src/StringAuditTrait.php new file mode 100644 index 00000000..b9cf94e7 --- /dev/null +++ b/src/StringAuditTrait.php @@ -0,0 +1,195 @@ +getReferences(); + + // File headers don't have any file references. + $location = $translation->hasReferences() ? '(' . implode( ':', $references[0] ) . ')' : ''; + + // Check 1: Flag strings with placeholders that should have translator comments. + if ( + ! $translation->hasExtractedComments() && + preg_match( $sprintf_placeholder_regex, $translation->getOriginal(), $placeholders ) >= 1 + ) { + $message = sprintf( + 'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s', + $translation->getOriginal(), + $location + ); + WP_CLI::warning( $message ); + } + + // Check 2: Flag strings with different translator comments. + if ( $translation->hasExtractedComments() ) { + $comments = $translation->getExtractedComments(); + + // Remove plugin/theme header information from comments. + $comments = array_filter( + $comments, + function ( $comment ) use ( $file_headers ) { + /** @var ParsedComment|string $comment */ + /** @var string $file_header */ + foreach ( $file_headers as $file_header ) { + if ( 0 === strpos( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $file_header ) ) { + return false; + } + } + + return $comment; + } + ); + + $unique_comments = array(); + + // Remove duplicate comments. + $comments = array_filter( + $comments, + function ( $comment ) use ( &$unique_comments ) { + /** @var ParsedComment|string $comment */ + if ( in_array( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $unique_comments, true ) ) { + return false; + } + + $unique_comments[] = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ); + + return $comment; + } + ); + + $comments_count = count( $comments ); + + if ( $comments_count > 1 ) { + $message = sprintf( + "The string \"%1\$s\" has %2\$d different translator comments. %3\$s\n%4\$s", + $translation->getOriginal(), + $comments_count, + $location, + implode( "\n", $unique_comments ) + ); + WP_CLI::warning( $message ); + } + } + + $non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) ); + $non_placeholder_content = preg_replace( $sprintf_placeholder_regex, '', $non_placeholder_content ); + + // Check 3: Flag empty strings without any translatable content. + if ( '' === $non_placeholder_content ) { + $message = sprintf( + 'Found string without translatable content. %s', + $location + ); + WP_CLI::warning( $message ); + } + + // Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s). + $unordered_matches_count = preg_match_all( $unordered_sprintf_placeholder_regex, $translation->getOriginal(), $unordered_matches ); + $unordered_matches = $unordered_matches[0]; + + if ( $unordered_matches_count >= 2 ) { + $message = sprintf( + 'Multiple placeholders should be ordered. %s', + $location + ); + WP_CLI::warning( $message ); + } + + if ( $translation->hasPlural() ) { + preg_match_all( $sprintf_placeholder_regex, $translation->getOriginal(), $single_placeholders ); + $single_placeholders = $single_placeholders[0]; + + preg_match_all( $sprintf_placeholder_regex, $translation->getPlural(), $plural_placeholders ); + $plural_placeholders = $plural_placeholders[0]; + + // see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals + if ( count( $single_placeholders ) < count( $plural_placeholders ) ) { + // Check 5: Flag things like _n( 'One comment', '%s Comments' ) + $message = sprintf( + 'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s', + $location + ); + WP_CLI::warning( $message ); + } else { + // Reordering is fine, but mismatched placeholders is probably wrong. + sort( $single_placeholders ); + sort( $plural_placeholders ); + + // Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' ) + if ( $single_placeholders !== $plural_placeholders ) { + $message = sprintf( + 'Mismatched placeholders for singular and plural string. %s', + $location + ); + WP_CLI::warning( $message ); + } + } + } + } + } +}