From 8b6460639f3b047f10e85332fcb2bbf89bc41d94 Mon Sep 17 00:00:00 2001
From: Saskia Teichmann ' . __( 'Your translations are all up to date.' ) . '' . __( 'Translations' ) . '
';
echo '
+ (%d)',
+ __( 'Translations' ),
+ number_format_i18n( $updates_count )
+ );
+ ?>
+
diff --git a/tests/phpunit/tests/admin/includesUpdate.php b/tests/phpunit/tests/admin/includesUpdate.php
new file mode 100644
index 0000000000000..029b170ab0f9f
--- /dev/null
+++ b/tests/phpunit/tests/admin/includesUpdate.php
@@ -0,0 +1,196 @@
+ array(
+ 'native_name' => 'Deutsch',
+ ),
+ )
+ );
+
+ set_site_transient(
+ 'update_core',
+ (object) array(
+ 'translations' => array(
+ array(
+ 'type' => 'core',
+ 'slug' => 'default',
+ 'language' => 'de_DE',
+ 'version' => '6.7-beta3',
+ ),
+ ),
+ )
+ );
+
+ set_site_transient(
+ 'update_plugins',
+ (object) array(
+ 'translations' => array(
+ array(
+ 'type' => 'plugin',
+ 'slug' => 'custom-internationalized-plugin',
+ 'language' => 'de_DE',
+ 'version' => '1.0.0',
+ ),
+ ),
+ )
+ );
+
+ set_site_transient(
+ 'update_themes',
+ (object) array(
+ 'translations' => array(
+ array(
+ 'type' => 'theme',
+ 'slug' => 'custom-internationalized-theme',
+ 'language' => 'de_DE',
+ 'version' => '1.0.0',
+ ),
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ array(
+ 'language' => 'Deutsch (de_DE)',
+ 'language_code' => 'de_DE',
+ 'name' => 'WordPress',
+ 'slug' => 'default',
+ 'type' => 'core',
+ 'version' => '6.7-beta3',
+ ),
+ array(
+ 'language' => 'Deutsch (de_DE)',
+ 'language_code' => 'de_DE',
+ 'name' => 'Custom Dummy Plugin',
+ 'slug' => 'custom-internationalized-plugin',
+ 'type' => 'plugin',
+ 'version' => '1.0.0',
+ ),
+ array(
+ 'language' => 'Deutsch (de_DE)',
+ 'language_code' => 'de_DE',
+ 'name' => 'Custom Internationalized Theme',
+ 'slug' => 'custom-internationalized-theme',
+ 'type' => 'theme',
+ 'version' => '1.0.0',
+ ),
+ ),
+ wp_get_translation_update_data()
+ );
+ }
+
+ /**
+ * @ticket 42281
+ */
+ public function test_wp_get_translation_update_data_falls_back_to_locale_and_slug() {
+ set_site_transient(
+ 'update_plugins',
+ (object) array(
+ 'translations' => array(
+ array(
+ 'type' => 'plugin',
+ 'slug' => 'missing-plugin',
+ 'language' => 'it_IT',
+ 'version' => '2.0.0',
+ ),
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ array(
+ 'language' => 'it_IT',
+ 'language_code' => 'it_IT',
+ 'name' => 'missing-plugin',
+ 'slug' => 'missing-plugin',
+ 'type' => 'plugin',
+ 'version' => '2.0.0',
+ ),
+ ),
+ wp_get_translation_update_data()
+ );
+ }
+
+ /**
+ * @ticket 42281
+ */
+ public function test_wp_get_translation_update_data_matches_plugin_update_slug_to_plugin_file() {
+ add_filter(
+ 'all_plugins',
+ static function () {
+ return array(
+ 'hello.php' => array(
+ 'Name' => 'Hello Dolly',
+ ),
+ );
+ }
+ );
+
+ set_site_transient(
+ 'update_plugins',
+ (object) array(
+ 'translations' => array(
+ array(
+ 'type' => 'plugin',
+ 'slug' => 'hello-dolly',
+ 'language' => 'de_DE',
+ 'version' => '1.7.2',
+ ),
+ ),
+ 'no_update' => array(
+ 'hello.php' => (object) array(
+ 'slug' => 'hello-dolly',
+ ),
+ ),
+ )
+ );
+
+ $this->assertSame(
+ array(
+ array(
+ 'language' => 'de_DE',
+ 'language_code' => 'de_DE',
+ 'name' => 'Hello Dolly',
+ 'slug' => 'hello-dolly',
+ 'type' => 'plugin',
+ 'version' => '1.7.2',
+ ),
+ ),
+ wp_get_translation_update_data()
+ );
+ }
+}
From c5077172d55bc75238b92dca3d6f77e568be747e Mon Sep 17 00:00:00 2001
From: Saskia Teichmann
' . __( 'Translations — The files translating WordPress into your language are updated for you whenever any other updates occur. But if these files are out of date, you can click the “Update Translations” button.' ) . '
'; + $updates_howto .= '' . __( 'Translations — Translation updates are selected for installation by default. Leave any translation updates unchecked if you want to install them later, then click the “Update Translations” button. Translation updates you leave unchecked will remain available until you select them.' ) . '
'; } get_current_screen()->add_help_tab( @@ -1116,6 +1158,15 @@ function do_undismiss_core_update() { } } + if ( isset( $_GET['translation_updates'] ) && 'deferred' === $_GET['translation_updates'] ) { + wp_admin_notice( + __( 'The unchecked translation updates will remain available until you select them.' ), + array( + 'type' => 'success', + ) + ); + } + $last_update_check = false; $current = get_site_transient( 'update_core' ); @@ -1299,6 +1350,55 @@ function do_undismiss_core_update() { check_admin_referer( 'upgrade-translations' ); + if ( empty( $_POST['translations'] ) ) { + wp_redirect( self_admin_url( 'update-core.php' ) ); + exit; + } + + $translation_updates = wp_get_translation_updates_by_id(); + + $translation_update_ids = array_unique( + array_map( + 'sanitize_text_field', + wp_unslash( (array) $_POST['translations'] ) + ) + ); + + $translation_updates = array_intersect_key( $translation_updates, array_flip( $translation_update_ids ) ); + + if ( empty( $translation_updates ) ) { + wp_redirect( self_admin_url( 'update-core.php' ) ); + exit; + } + + $selected_translation_updates = array(); + + if ( ! empty( $_POST['checked'] ) ) { + $selected_translation_update_ids = array_unique( + array_map( + 'sanitize_text_field', + wp_unslash( (array) $_POST['checked'] ) + ) + ); + + $selected_translation_updates = array_intersect_key( $translation_updates, array_flip( $selected_translation_update_ids ) ); + } + + $current_translation_updates = wp_get_translation_updates_by_id(); + $deferred_translation_updates = array_intersect_key( + $current_translation_updates, + wp_get_deferred_translation_updates( $current_translation_updates ) + ); + $deferred_translation_updates = array_diff_key( $deferred_translation_updates, $translation_updates ); + $deferred_translation_updates += array_diff_key( $translation_updates, $selected_translation_updates ); + + wp_set_deferred_translation_updates( $deferred_translation_updates ); + + if ( empty( $selected_translation_updates ) ) { + wp_redirect( add_query_arg( 'translation_updates', 'deferred', self_admin_url( 'update-core.php' ) ) ); + exit; + } + require_once ABSPATH . 'wp-admin/admin-header.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; @@ -1308,7 +1408,7 @@ function do_undismiss_core_update() { $context = WP_LANG_DIR; $upgrader = new Language_Pack_Upgrader( new Language_Pack_Upgrader_Skin( compact( 'url', 'nonce', 'title', 'context' ) ) ); - $result = $upgrader->bulk_upgrade(); + $result = $upgrader->bulk_upgrade( array_values( $selected_translation_updates ) ); wp_localize_script( 'updates', diff --git a/src/wp-includes/update.php b/src/wp-includes/update.php index b7bf5a03780e7..fb277f778a333 100644 --- a/src/wp-includes/update.php +++ b/src/wp-includes/update.php @@ -920,6 +920,144 @@ function wp_get_translation_updates() { return $updates; } +/** + * Gets a stable identifier for a translation update. + * + * @since 7.1.0 + * + * @param array|object $update Translation update data. + * @return string Translation update identifier. + */ +function wp_get_translation_update_id( $update ) { + $update = (object) $update; + + return md5( + wp_json_encode( + array( + 'type' => $update->type ?? '', + 'slug' => $update->slug ?? '', + 'language' => $update->language ?? '', + 'version' => $update->version ?? '', + ) + ) + ); +} + +/** + * Gets translation updates keyed by their stable identifiers. + * + * @since 7.1.0 + * + * @param object[]|null $updates Optional. Translation update objects. Default null. + * @return object[] Translation updates keyed by identifier. + */ +function wp_get_translation_updates_by_id( $updates = null ) { + if ( null === $updates ) { + $updates = wp_get_translation_updates(); + } + + $translation_updates = array(); + + foreach ( (array) $updates as $update ) { + $translation_updates[ wp_get_translation_update_id( $update ) ] = (object) $update; + } + + return $translation_updates; +} + +/** + * Gets deferred translation updates. + * + * Deferred translation updates remain available for later installation and are + * skipped by background translation updates until they are selected again. + * + * @since 7.1.0 + * + * @param object[]|null $updates Optional. Translation update objects used to filter deferred updates + * to currently available updates. Default null. + * @return array[] Deferred translation updates keyed by identifier. + */ +function wp_get_deferred_translation_updates( $updates = null ) { + $stored_updates = get_site_option( 'deferred_translation_updates', array() ); + + if ( ! is_array( $stored_updates ) ) { + return array(); + } + + $deferred_updates = array(); + + foreach ( $stored_updates as $stored_update ) { + if ( ! is_array( $stored_update ) ) { + continue; + } + + $stored_update = (object) array( + 'type' => $stored_update['type'] ?? '', + 'slug' => $stored_update['slug'] ?? '', + 'language' => $stored_update['language'] ?? '', + 'version' => $stored_update['version'] ?? '', + ); + + $deferred_updates[ wp_get_translation_update_id( $stored_update ) ] = (array) $stored_update; + } + + if ( null !== $updates ) { + $deferred_updates = array_intersect_key( $deferred_updates, wp_get_translation_updates_by_id( $updates ) ); + } + + return $deferred_updates; +} + +/** + * Determines whether a translation update has been deferred. + * + * @since 7.1.0 + * + * @param array|object $update Translation update data. + * @param array[]|null $deferred_updates Optional. Deferred translation updates keyed by identifier. + * Default null. + * @return bool Whether the translation update has been deferred. + */ +function wp_is_translation_update_deferred( $update, $deferred_updates = null ) { + if ( null === $deferred_updates ) { + $deferred_updates = wp_get_deferred_translation_updates(); + } + + return isset( $deferred_updates[ wp_get_translation_update_id( $update ) ] ); +} + +/** + * Stores deferred translation updates. + * + * @since 7.1.0 + * + * @param object[]|array[] $updates Translation updates to defer. + * @return void + */ +function wp_set_deferred_translation_updates( $updates ) { + $deferred_updates = array(); + + foreach ( (array) $updates as $update ) { + $update = (object) $update; + + $translation_update = array( + 'type' => $update->type ?? '', + 'slug' => $update->slug ?? '', + 'language' => $update->language ?? '', + 'version' => $update->version ?? '', + ); + + $deferred_updates[ wp_get_translation_update_id( $translation_update ) ] = $translation_update; + } + + if ( empty( $deferred_updates ) ) { + delete_site_option( 'deferred_translation_updates' ); + return; + } + + update_site_option( 'deferred_translation_updates', $deferred_updates ); +} + /** * Collects counts and UI strings for available updates. * diff --git a/tests/phpunit/tests/admin/includesUpdate.php b/tests/phpunit/tests/admin/includesUpdate.php index 029b170ab0f9f..f94be8a866ade 100644 --- a/tests/phpunit/tests/admin/includesUpdate.php +++ b/tests/phpunit/tests/admin/includesUpdate.php @@ -4,9 +4,14 @@ * @group admin * @group upgrade * + * @covers ::wp_get_deferred_translation_updates * @covers ::wp_get_translation_update_data + * @covers ::wp_get_translation_update_id * @covers ::wp_get_translation_update_language * @covers ::wp_get_translation_update_name + * @covers ::wp_get_translation_updates_by_id + * @covers ::wp_is_translation_update_deferred + * @covers ::wp_set_deferred_translation_updates */ class Tests_Admin_IncludesUpdate extends WP_UnitTestCase { /** @@ -17,6 +22,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } public function tear_down() { + delete_site_option( 'deferred_translation_updates' ); delete_site_transient( 'available_translations' ); delete_site_transient( 'update_core' ); delete_site_transient( 'update_plugins' ); @@ -30,6 +36,27 @@ public function tear_down() { * @ticket 42281 */ public function test_wp_get_translation_update_data_returns_display_ready_translation_updates() { + $core_update = (object) array( + 'type' => 'core', + 'slug' => 'default', + 'language' => 'de_DE', + 'version' => '6.7-beta3', + ); + + $plugin_update = (object) array( + 'type' => 'plugin', + 'slug' => 'custom-internationalized-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ); + + $theme_update = (object) array( + 'type' => 'theme', + 'slug' => 'custom-internationalized-theme', + 'language' => 'de_DE', + 'version' => '1.0.0', + ); + set_site_transient( 'available_translations', array( @@ -43,12 +70,7 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl 'update_core', (object) array( 'translations' => array( - array( - 'type' => 'core', - 'slug' => 'default', - 'language' => 'de_DE', - 'version' => '6.7-beta3', - ), + (array) $core_update, ), ) ); @@ -57,12 +79,7 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl 'update_plugins', (object) array( 'translations' => array( - array( - 'type' => 'plugin', - 'slug' => 'custom-internationalized-plugin', - 'language' => 'de_DE', - 'version' => '1.0.0', - ), + (array) $plugin_update, ), ) ); @@ -71,12 +88,7 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl 'update_themes', (object) array( 'translations' => array( - array( - 'type' => 'theme', - 'slug' => 'custom-internationalized-theme', - 'language' => 'de_DE', - 'version' => '1.0.0', - ), + (array) $theme_update, ), ) ); @@ -84,6 +96,9 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl $this->assertSame( array( array( + 'checked' => true, + 'deferred' => false, + 'id' => wp_get_translation_update_id( $core_update ), 'language' => 'Deutsch (de_DE)', 'language_code' => 'de_DE', 'name' => 'WordPress', @@ -92,6 +107,9 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl 'version' => '6.7-beta3', ), array( + 'checked' => true, + 'deferred' => false, + 'id' => wp_get_translation_update_id( $plugin_update ), 'language' => 'Deutsch (de_DE)', 'language_code' => 'de_DE', 'name' => 'Custom Dummy Plugin', @@ -100,6 +118,9 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl 'version' => '1.0.0', ), array( + 'checked' => true, + 'deferred' => false, + 'id' => wp_get_translation_update_id( $theme_update ), 'language' => 'Deutsch (de_DE)', 'language_code' => 'de_DE', 'name' => 'Custom Internationalized Theme', @@ -116,16 +137,18 @@ public function test_wp_get_translation_update_data_returns_display_ready_transl * @ticket 42281 */ public function test_wp_get_translation_update_data_falls_back_to_locale_and_slug() { + $plugin_update = (object) array( + 'type' => 'plugin', + 'slug' => 'missing-plugin', + 'language' => 'it_IT', + 'version' => '2.0.0', + ); + set_site_transient( 'update_plugins', (object) array( 'translations' => array( - array( - 'type' => 'plugin', - 'slug' => 'missing-plugin', - 'language' => 'it_IT', - 'version' => '2.0.0', - ), + (array) $plugin_update, ), ) ); @@ -133,6 +156,9 @@ public function test_wp_get_translation_update_data_falls_back_to_locale_and_slu $this->assertSame( array( array( + 'checked' => true, + 'deferred' => false, + 'id' => wp_get_translation_update_id( $plugin_update ), 'language' => 'it_IT', 'language_code' => 'it_IT', 'name' => 'missing-plugin', @@ -149,6 +175,13 @@ public function test_wp_get_translation_update_data_falls_back_to_locale_and_slu * @ticket 42281 */ public function test_wp_get_translation_update_data_matches_plugin_update_slug_to_plugin_file() { + $plugin_update = (object) array( + 'type' => 'plugin', + 'slug' => 'hello-dolly', + 'language' => 'de_DE', + 'version' => '1.7.2', + ); + add_filter( 'all_plugins', static function () { @@ -164,12 +197,7 @@ static function () { 'update_plugins', (object) array( 'translations' => array( - array( - 'type' => 'plugin', - 'slug' => 'hello-dolly', - 'language' => 'de_DE', - 'version' => '1.7.2', - ), + (array) $plugin_update, ), 'no_update' => array( 'hello.php' => (object) array( @@ -182,6 +210,9 @@ static function () { $this->assertSame( array( array( + 'checked' => true, + 'deferred' => false, + 'id' => wp_get_translation_update_id( $plugin_update ), 'language' => 'de_DE', 'language_code' => 'de_DE', 'name' => 'Hello Dolly', @@ -193,4 +224,108 @@ static function () { wp_get_translation_update_data() ); } + + /** + * @ticket 42281 + */ + public function test_wp_get_translation_updates_by_id_keys_updates_by_identifier() { + $core_update = (object) array( + 'type' => 'core', + 'slug' => 'default', + 'language' => 'de_DE', + 'version' => '6.7-beta3', + ); + + $this->assertSame( + array( + wp_get_translation_update_id( $core_update ) => $core_update, + ), + wp_get_translation_updates_by_id( array( $core_update ) ) + ); + } + + /** + * @ticket 42281 + */ + public function test_wp_get_translation_update_data_marks_deferred_translation_updates() { + $plugin_update = (object) array( + 'type' => 'plugin', + 'slug' => 'deferred-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ); + + set_site_transient( + 'update_plugins', + (object) array( + 'translations' => array( + (array) $plugin_update, + ), + ) + ); + + wp_set_deferred_translation_updates( array( $plugin_update ) ); + + $this->assertSame( + array( + array( + 'checked' => false, + 'deferred' => true, + 'id' => wp_get_translation_update_id( $plugin_update ), + 'language' => 'de_DE', + 'language_code' => 'de_DE', + 'name' => 'deferred-plugin', + 'slug' => 'deferred-plugin', + 'type' => 'plugin', + 'version' => '1.0.0', + ), + ), + wp_get_translation_update_data() + ); + } + + /** + * @ticket 42281 + */ + public function test_wp_get_deferred_translation_updates_filters_to_available_updates() { + $available_update = (object) array( + 'type' => 'plugin', + 'slug' => 'available-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ); + + $stale_update = (object) array( + 'type' => 'plugin', + 'slug' => 'stale-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ); + + wp_set_deferred_translation_updates( array( $available_update, $stale_update ) ); + + $this->assertSame( + array( + wp_get_translation_update_id( $available_update ) => array( + 'type' => 'plugin', + 'slug' => 'available-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ), + ), + wp_get_deferred_translation_updates( array( $available_update ) ) + ); + + $this->assertTrue( wp_is_translation_update_deferred( $available_update ) ); + $this->assertFalse( + wp_is_translation_update_deferred( + (object) array( + 'type' => 'plugin', + 'slug' => 'different-plugin', + 'language' => 'de_DE', + 'version' => '1.0.0', + ) + ) + ); + } }