diff --git a/features/updatepo.feature b/features/updatepo.feature index 6aba5e30..d648058b 100644 --- a/features/updatepo.feature +++ b/features/updatepo.feature @@ -465,3 +465,145 @@ Feature: Update existing PO files from a POT file """ "X-Domain: foo-plugin\n" """ + + Scenario: Preserves obsolete translations and file-level comments with --no-purge + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.pot 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" + "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" + + #: foo-plugin.php:1 + msgid "Some string" + msgstr "" + """ + 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:1 + msgid "Some string" + msgstr "Some translated string" + + #~ msgid "Obsolete string" + #~ msgstr "Veralteter String" + """ + + When I run `wp i18n update-po foo-plugin/foo-plugin.pot foo-plugin/foo-plugin-de_DE.po --no-purge` + Then STDOUT should be: + """ + Success: Updated 1 file. + """ + And STDERR should be empty + And the foo-plugin/foo-plugin-de_DE.po file should contain: + """ + # Copyright (C) 2018 Foo Plugin + # This file is distributed under the same license as the Foo Plugin package. + """ + And the foo-plugin/foo-plugin-de_DE.po file should contain: + """ + #~ msgid "Obsolete string" + #~ msgstr "Veralteter String" + """ + And the foo-plugin/foo-plugin-de_DE.po file should contain: + """ + #: foo-plugin.php:1 + msgid "Some string" + msgstr "Some translated string" + """ + + Scenario: Removes obsolete translations and comments by default + Given an empty foo-plugin directory + And a foo-plugin/foo-plugin.pot file: + """ + 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" + "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" + + #: foo-plugin.php:1 + msgid "Some string" + msgstr "" + """ + 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:1 + msgid "Some string" + msgstr "Some translated string" + + #~ msgid "Obsolete string" + #~ msgstr "Veralteter String" + """ + + When I run `wp i18n update-po foo-plugin/foo-plugin.pot foo-plugin/foo-plugin-de_DE.po` + Then STDOUT should be: + """ + Success: Updated 1 file. + """ + And STDERR should be empty + And the foo-plugin/foo-plugin-de_DE.po file should not contain: + """ + # Copyright (C) 2018 Foo Plugin + """ + And the foo-plugin/foo-plugin-de_DE.po file should not contain: + """ + #~ msgid "Obsolete string" + """ + And the foo-plugin/foo-plugin-de_DE.po file should contain: + """ + #: foo-plugin.php:1 + msgid "Some string" + msgstr "Some translated string" + """ diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index 5af6b996..08822311 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -27,6 +27,11 @@ class UpdatePoCommand extends WP_CLI_Command { * : PO file to update or a directory containing multiple PO files. * Defaults to all PO files in the source directory. * + * [--purge] + * : Remove obsolete strings and replace translator comments. Defaults to true. + * By default, strings not found in the POT file are removed, and translator comments are replaced with those from the POT file. + * Use `--no-purge` to preserve obsolete translations (marked with #~) and existing translator comments like copyright notices. + * * ## EXAMPLES * * # Update all PO files from a POT file in the current directory. @@ -41,6 +46,10 @@ class UpdatePoCommand extends WP_CLI_Command { * $ wp i18n update-po example-plugin.pot languages * Success: Updated 2 files. * + * # Update PO files while keeping obsolete strings and translator comments. + * $ wp i18n update-po example-plugin.pot --no-purge + * Success: Updated 3 files. + * * @when before_wp_load * * @throws WP_CLI\ExitException @@ -69,6 +78,16 @@ public function __invoke( $args, $assoc_args ) { $pot_translations = Translations::fromPoFile( $source ); + // Build merge flags based on options + $merge_flags = Merge::ADD | Merge::EXTRACTED_COMMENTS_THEIRS | Merge::REFERENCES_THEIRS | Merge::DOMAIN_OVERRIDE; + + $purge = Utils\get_flag_value( $assoc_args, 'purge', true ); + + if ( $purge ) { + // By default, remove obsolete entries and replace translator comments + $merge_flags |= Merge::REMOVE | Merge::COMMENTS_THEIRS; + } + $result_count = 0; /** @var DirectoryIterator $file */ foreach ( $files as $file ) { @@ -81,10 +100,16 @@ public function __invoke( $args, $assoc_args ) { continue; } + // Preserve file-level comments when --no-purge is set + $file_comments = ''; + if ( ! $purge ) { + $file_comments = $this->extract_file_comments( $file->getPathname() ); + } + $po_translations = Translations::fromPoFile( $file->getPathname() ); $po_translations->mergeWith( $pot_translations, - Merge::ADD | Merge::REMOVE | Merge::COMMENTS_THEIRS | Merge::EXTRACTED_COMMENTS_THEIRS | Merge::REFERENCES_THEIRS | Merge::DOMAIN_OVERRIDE + $merge_flags ); if ( ! $po_translations->toPoFile( $file->getPathname() ) ) { @@ -92,9 +117,82 @@ public function __invoke( $args, $assoc_args ) { continue; } + // Restore file-level comments when --no-purge is set + if ( ! $purge && ! empty( $file_comments ) ) { + $this->restore_file_comments( $file->getPathname(), $file_comments ); + } + ++$result_count; } WP_CLI::success( sprintf( 'Updated %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) ); } + + /** + * Extract file-level comments from a PO file. + * + * These are comments that appear before the first msgid in the file. + * + * @param string $file_path Path to the PO file. + * @return string The file-level comments. + */ + private function extract_file_comments( $file_path ) { + $content = file_get_contents( $file_path ); + if ( false === $content ) { + return ''; + } + + $lines = explode( "\n", $content ); + $file_comments = []; + $found_msgid = false; + + foreach ( $lines as $line ) { + $trimmed = trim( $line ); + + // Stop when we hit the first msgid + if ( preg_match( '/^msgid\s/', $trimmed ) ) { + $found_msgid = true; + break; + } + + // Collect comment lines + if ( preg_match( '/^#([^.,:~]|$)/', $trimmed ) ) { + $file_comments[] = $line; + } + } + + return $found_msgid && ! empty( $file_comments ) ? implode( "\n", $file_comments ) . "\n" : ''; + } + + /** + * Restore file-level comments to a PO file. + * + * @param string $file_path Path to the PO file. + * @param string $comments The file-level comments to restore. + * @return bool True on success, false on failure. + */ + private function restore_file_comments( $file_path, $comments ) { + $content = file_get_contents( $file_path ); + if ( false === $content ) { + return false; + } + + // Prepend the comments to the file content + $updated_content = $comments . $content; + + // Use atomic file operation with temporary file + $temp_file = $file_path . '.tmp'; + if ( false === file_put_contents( $temp_file, $updated_content ) ) { + return false; + } + + // Rename is atomic on most filesystems + if ( ! rename( $temp_file, $file_path ) ) { + // Clean up temp file on failure + @unlink( $temp_file ); + return false; + } + + return true; + } }