From d06edb0942d6da80236a6da36c0fab0704fb9f9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:43:06 +0000 Subject: [PATCH 1/7] Initial plan From ac39abd67120a77b325207dc3948ca614ea37a0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:00:51 +0000 Subject: [PATCH 2/7] Implement change detection for update-po command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/updatepo.feature | 139 ++++++++++++++++++++++++++++++++++++++ src/UpdatePoCommand.php | 33 ++++++++- 2 files changed, 169 insertions(+), 3 deletions(-) diff --git a/features/updatepo.feature b/features/updatepo.feature index 6aba5e30..8ef616b2 100644 --- a/features/updatepo.feature +++ b/features/updatepo.feature @@ -465,3 +465,142 @@ Feature: Update existing PO files from a POT file """ "X-Domain: foo-plugin\n" """ + + Scenario: Reports unchanged files when POT hasn't changed + 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" + """ + + When I run `wp i18n update-po foo-plugin/foo-plugin.pot` + Then STDOUT should be: + """ + Success: Updated 0 files. 1 file unchanged. + """ + And STDERR should be empty + + Scenario: Reports both updated and unchanged files + 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 "" + + #: foo-plugin.php:15 + msgid "Another new 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:10 + msgid "Some string" + msgstr "Some translated string" + """ + And a foo-plugin/foo-plugin-es_ES.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: es_ES\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" + + #: foo-plugin.php:15 + msgid "Another new string" + msgstr "Otra nueva cadena" + """ + + When I run `wp i18n update-po foo-plugin/foo-plugin.pot` + Then STDOUT should be: + """ + Success: Updated 1 file. 1 file unchanged. + """ + And STDERR should be empty diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index 5af6b996..664d777f 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -41,6 +41,10 @@ class UpdatePoCommand extends WP_CLI_Command { * $ wp i18n update-po example-plugin.pot languages * Success: Updated 2 files. * + * # Shows message when some files don't need updating. + * $ wp i18n update-po example-plugin.pot languages + * Success: Updated 2 files. 1 file unchanged. + * * @when before_wp_load * * @throws WP_CLI\ExitException @@ -69,7 +73,8 @@ public function __invoke( $args, $assoc_args ) { $pot_translations = Translations::fromPoFile( $source ); - $result_count = 0; + $updated_count = 0; + $unchanged_count = 0; /** @var DirectoryIterator $file */ foreach ( $files as $file ) { if ( 'po' !== $file->getExtension() ) { @@ -81,6 +86,9 @@ public function __invoke( $args, $assoc_args ) { continue; } + // Read the original file content before merging. + $original_content = file_get_contents( $file->getPathname() ); + $po_translations = Translations::fromPoFile( $file->getPathname() ); $po_translations->mergeWith( $pot_translations, @@ -92,9 +100,28 @@ public function __invoke( $args, $assoc_args ) { continue; } - ++$result_count; + // Read the new file content after merging. + $new_content = file_get_contents( $file->getPathname() ); + + // Check if the file actually changed. + if ( $original_content !== $new_content ) { + ++$updated_count; + } else { + ++$unchanged_count; + } + } + + // Build the success message. + $message_parts = array(); + if ( $updated_count > 0 ) { + $message_parts[] = sprintf( 'Updated %d %s', $updated_count, Utils\pluralize( 'file', $updated_count ) ); + } else { + $message_parts[] = sprintf( 'Updated %d files', $updated_count ); + } + if ( $unchanged_count > 0 ) { + $message_parts[] = sprintf( '%d %s unchanged', $unchanged_count, Utils\pluralize( 'file', $unchanged_count ) ); } - WP_CLI::success( sprintf( 'Updated %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) ); + WP_CLI::success( implode( '. ', $message_parts ) . '.' ); } } From b09b981122430ede06a0bcdfc1faef18ed6ef3af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:03:47 +0000 Subject: [PATCH 3/7] Simplify message formatting logic Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/UpdatePoCommand.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index 664d777f..9fb32158 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -113,11 +113,7 @@ public function __invoke( $args, $assoc_args ) { // Build the success message. $message_parts = array(); - if ( $updated_count > 0 ) { - $message_parts[] = sprintf( 'Updated %d %s', $updated_count, Utils\pluralize( 'file', $updated_count ) ); - } else { - $message_parts[] = sprintf( 'Updated %d files', $updated_count ); - } + $message_parts[] = sprintf( 'Updated %d %s', $updated_count, Utils\pluralize( 'file', $updated_count ) ); if ( $unchanged_count > 0 ) { $message_parts[] = sprintf( '%d %s unchanged', $unchanged_count, Utils\pluralize( 'file', $unchanged_count ) ); } From 82e097fa4bee0cc44eaa768117db3c9b826e82c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:43:50 +0000 Subject: [PATCH 4/7] Fix change detection by comparing Translation objects instead of file content Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/UpdatePoCommand.php | 89 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index 9fb32158..66bc71cd 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -86,25 +86,23 @@ public function __invoke( $args, $assoc_args ) { continue; } - // Read the original file content before merging. - $original_content = file_get_contents( $file->getPathname() ); + $po_translations = Translations::fromPoFile( $file->getPathname() ); + $original_translations = clone $po_translations; - $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 ); + // Check if the translations actually changed by comparing the objects. + $has_changes = $this->translations_differ( $original_translations, $po_translations ); + if ( ! $po_translations->toPoFile( $file->getPathname() ) ) { WP_CLI::warning( sprintf( 'Could not update file %s', $file->getPathname() ) ); continue; } - // Read the new file content after merging. - $new_content = file_get_contents( $file->getPathname() ); - - // Check if the file actually changed. - if ( $original_content !== $new_content ) { + if ( $has_changes ) { ++$updated_count; } else { ++$unchanged_count; @@ -120,4 +118,79 @@ public function __invoke( $args, $assoc_args ) { WP_CLI::success( implode( '. ', $message_parts ) . '.' ); } + + /** + * Check if two Translations objects differ. + * + * @param Translations $original Original translations. + * @param Translations $updated Updated translations. + * @return bool True if translations differ, false otherwise. + */ + private function translations_differ( Translations $original, Translations $updated ) { + // Quick check: if counts differ, they're different. + if ( count( $original ) !== count( $updated ) ) { + return true; + } + + // Compare each translation entry. + foreach ( $original as $translation ) { + $context = $translation->getContext(); + $original_str = $translation->getOriginal(); + + // Find the corresponding translation in the updated set. + $updated_translation = $updated->find( $context, $original_str ); + + // If translation doesn't exist in updated set, they differ. + if ( ! $updated_translation ) { + return true; + } + + // Compare translation strings. + if ( $translation->getTranslation() !== $updated_translation->getTranslation() ) { + return true; + } + + // Compare plural translations if they exist. + $original_plurals = $translation->getPluralTranslations(); + $updated_plurals = $updated_translation->getPluralTranslations(); + + if ( $original_plurals !== $updated_plurals ) { + return true; + } + + // Compare references (source code locations). + $original_refs = $translation->getReferences(); + $updated_refs = $updated_translation->getReferences(); + + sort( $original_refs ); + sort( $updated_refs ); + + if ( $original_refs !== $updated_refs ) { + return true; + } + + // Compare comments. + if ( $translation->getExtractedComments() !== $updated_translation->getExtractedComments() ) { + return true; + } + + if ( $translation->getTranslatorComments() !== $updated_translation->getTranslatorComments() ) { + return true; + } + } + + // Check if updated has any translations not in original. + foreach ( $updated as $translation ) { + $context = $translation->getContext(); + $original_str = $translation->getOriginal(); + + $original_translation = $original->find( $context, $original_str ); + + if ( ! $original_translation ) { + return true; + } + } + + return false; + } } From d1e260593f7c4a8455b6cc0a35c4b100aa89306a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:38:34 +0000 Subject: [PATCH 5/7] Fix method name: use getComments() instead of getTranslatorComments() Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/UpdatePoCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index 66bc71cd..b7893a47 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -174,7 +174,7 @@ private function translations_differ( Translations $original, Translations $upda return true; } - if ( $translation->getTranslatorComments() !== $updated_translation->getTranslatorComments() ) { + if ( $translation->getComments() !== $updated_translation->getComments() ) { return true; } } From 46e34ad815845f3a020cb88c3941cada1aa2058a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 3 Nov 2025 21:42:02 +0100 Subject: [PATCH 6/7] Lint fix --- src/UpdatePoCommand.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index b7893a47..f01903d6 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -86,8 +86,8 @@ public function __invoke( $args, $assoc_args ) { continue; } - $po_translations = Translations::fromPoFile( $file->getPathname() ); - $original_translations = clone $po_translations; + $po_translations = Translations::fromPoFile( $file->getPathname() ); + $original_translations = clone $po_translations; $po_translations->mergeWith( $pot_translations, @@ -110,7 +110,7 @@ public function __invoke( $args, $assoc_args ) { } // Build the success message. - $message_parts = array(); + $message_parts = array(); $message_parts[] = sprintf( 'Updated %d %s', $updated_count, Utils\pluralize( 'file', $updated_count ) ); if ( $unchanged_count > 0 ) { $message_parts[] = sprintf( '%d %s unchanged', $unchanged_count, Utils\pluralize( 'file', $unchanged_count ) ); @@ -134,7 +134,7 @@ private function translations_differ( Translations $original, Translations $upda // Compare each translation entry. foreach ( $original as $translation ) { - $context = $translation->getContext(); + $context = $translation->getContext(); $original_str = $translation->getOriginal(); // Find the corresponding translation in the updated set. @@ -181,7 +181,7 @@ private function translations_differ( Translations $original, Translations $upda // Check if updated has any translations not in original. foreach ( $updated as $translation ) { - $context = $translation->getContext(); + $context = $translation->getContext(); $original_str = $translation->getOriginal(); $original_translation = $original->find( $context, $original_str ); From 280cb62fa2e3f9733e6be7af24f2ee7338073090 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 19 Dec 2025 14:36:26 +0100 Subject: [PATCH 7/7] Update test --- features/updatepo.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/updatepo.feature b/features/updatepo.feature index 297ca2b2..4dad894c 100644 --- a/features/updatepo.feature +++ b/features/updatepo.feature @@ -516,7 +516,7 @@ Feature: Update existing PO files from a POT file When I run `wp i18n update-po foo-plugin/foo-plugin.pot` Then STDOUT should be: """ - Success: Updated 1 file. + Success: Updated 0 files. 1 file unchanged. """ And STDERR should be empty And the foo-plugin/foo-plugin-de_DE.po file should contain: