Commit f80e9dde436 for woocommerce
commit f80e9dde4369a9869fac883b32781d8bfe68d0ed
Author: Ján Mikláš <neosinner@gmail.com>
Date: Tue Nov 10 16:32:22 2026 +0100
Handle malformed translation revision dates during updates (#70723)
* fix: handle malformed translation revision dates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NM9YXuu5Fm1y6SFnuH2t8U
* chore: add translation updater changelog
* Update plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
Co-authored-by: Justin P <228780+layoutd@users.noreply.github.com>
* Update plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
Co-authored-by: Justin P <228780+layoutd@users.noreply.github.com>
* Update plugins/woocommerce/changelog/strprod-1100-woocommerce-product-add-ons-fr_fr-translation-ships
Co-authored-by: Justin P <228780+layoutd@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Justin P <228780+layoutd@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/strprod-1100-woocommerce-product-add-ons-fr_fr-translation-ships b/plugins/woocommerce/changelog/strprod-1100-woocommerce-product-add-ons-fr_fr-translation-ships
new file mode 100644
index 00000000000..d137c54e143
--- /dev/null
+++ b/plugins/woocommerce/changelog/strprod-1100-woocommerce-product-add-ons-fr_fr-translation-ships
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix a fatal error during plugin update checks when an installed translation file has an invalid date.
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
index 3071438a351..527cf469109 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
@@ -1044,13 +1044,24 @@ class WC_Helper_Updater {
foreach ( $response['data'] as $plugin_name => $language_packs ) {
foreach ( $language_packs as $language_pack ) {
// Maybe we have this language pack already installed so lets check revision date.
- if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) {
- $installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] );
- $new_translation_revision_time = new DateTime( $language_pack['last_modified'] );
- // Skip if translation language pack is not newer than what is installed already.
- if ( $new_translation_revision_time <= $installed_translation_revision_time ) {
+ if ( isset( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ] ) ) {
+ $remote_revision = $language_pack['last_modified'] ?? '';
+ if ( ! is_string( $remote_revision ) || '' === trim( $remote_revision ) ) {
continue;
}
+ $new_translation_revision_time = date_create( $remote_revision );
+ if ( false === $new_translation_revision_time ) {
+ continue;
+ }
+
+ // Intended: an empty or invalid installed date lets a valid pack replace it.
+ $installed_revision = $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] ?? '';
+ if ( is_string( $installed_revision ) && '' !== trim( $installed_revision ) ) {
+ $installed_translation_revision_time = date_create( $installed_revision );
+ if ( false !== $installed_translation_revision_time && $new_translation_revision_time <= $installed_translation_revision_time ) {
+ continue;
+ }
+ }
}
$translations[] = array(
'type' => 'plugin',
diff --git a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
index b8ed8299f2d..731710c0666 100644
--- a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
@@ -63,6 +63,89 @@ class WC_Helper_Updater_Test extends WC_Unit_Test_Case {
}
}
+ /**
+ * Tests translation updates with malformed installed or remote revision dates.
+ *
+ * @dataProvider translation_revision_dates
+ * @param string|null $installed_revision Date in the installed PO file, or null when the locale is not installed.
+ * @param string $remote_revision Date reported by the update service.
+ * @param bool $expect_update Whether the update should be offered.
+ */
+ public function test_translation_update_revision_dates( ?string $installed_revision, string $remote_revision, bool $expect_update ): void {
+ $plugin = $this->mock_local_woo_plugin();
+ $lang_dir = WP_LANG_DIR . '/plugins/';
+ $pack = $lang_dir . 'woo-test-plugin-fr_FR';
+ wp_mkdir_p( $lang_dir );
+ wp_delete_file( $pack . '.po' );
+ if ( null !== $installed_revision ) {
+ file_put_contents( $pack . '.po', "\"PO-Revision-Date: {$installed_revision}\\n\"\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture.
+ }
+
+ $files = static fn( $files, $path ) => $lang_dir === $path ? array( $pack . '.mo' ) : $files;
+ $active = static fn() => array( $plugin );
+ $locales = static fn() => array( 'fr_FR' );
+ $opt_in = static fn() => true;
+ $mock = static function ( $response, $args, $url ) use ( $remote_revision ) {
+ if ( 'https://translate.wordpress.com/api/translations-updates/woocommerce' !== $url ) {
+ return $response;
+ }
+ return array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode(
+ array(
+ 'data' => array(
+ 'woo-test-plugin' => array(
+ array(
+ 'wp_locale' => 'fr_FR',
+ 'last_modified' => $remote_revision,
+ 'version' => '1.0.0',
+ 'package' => 'https://example.test/fr_FR.zip',
+ ),
+ ),
+ ),
+ )
+ ),
+ );
+ };
+
+ add_filter( 'pre_get_language_files_from_path', $files, 10, 2 );
+ add_filter( 'pre_option_active_plugins', $active );
+ add_filter( 'plugins_update_check_locales', $locales );
+ add_filter( 'woocommerce_translations_updates_for_woo-test-plugin', $opt_in );
+ add_filter( 'pre_http_request', $mock, 10, 3 );
+
+ try {
+ $this->assertSame( $installed_revision, wp_get_installed_translations( 'plugins' )['woo-test-plugin']['fr_FR']['PO-Revision-Date'] ?? null );
+ $updates = WC_Helper_Updater::get_translations_update_data();
+ $this->assertSame( $expect_update ? array( 'fr_FR' ) : array(), array_column( $updates, 'language' ) );
+ } finally {
+ remove_filter( 'pre_get_language_files_from_path', $files );
+ remove_filter( 'pre_option_active_plugins', $active );
+ remove_filter( 'plugins_update_check_locales', $locales );
+ remove_filter( 'woocommerce_translations_updates_for_woo-test-plugin', $opt_in );
+ remove_filter( 'pre_http_request', $mock );
+ wp_delete_file( $pack . '.po' );
+ }
+ }
+
+ /**
+ * Supplies installed and remote translation revision dates.
+ *
+ * @return array
+ */
+ public function translation_revision_dates(): array {
+ return array(
+ 'replace invalid installed date' => array( '2018-MO-DA HO:MI+ZONE', '2026-02-01 00:00+0000', true ),
+ 'skip invalid remote date' => array( '2026-01-01 00:00+0000', 'invalid', false ),
+ 'offer newer update' => array( '2026-01-01 00:00+0000', '2026-02-01 00:00+0000', true ),
+ 'keep newer installed pack' => array( '2026-02-01 00:00+0000', '2026-01-01 00:00+0000', false ),
+ 'replace empty installed date' => array( '', '2026-02-01 00:00+0000', true ),
+ 'skip empty remote date' => array( '2026-01-01 00:00+0000', '', false ),
+ 'install without remote date' => array( null, '', true ),
+ 'install with invalid remote' => array( null, 'invalid', true ),
+ );
+ }
+
/**
* Unregister and delete the fixture theme, when one was created.
*/