Commit 1d5dfb35e9 for wordpress.org
commit 1d5dfb35e932554a5305f511b8397d7d5b011a1a
Author: audrasjb <audrasjb@1a063a9b-81f0-0310-95a4-ce76da25c4cd>
Date: Tue Nov 3 13:40:02 2026 +0000
Upgrade/Install: Show and allow to select translation updates on the Updates screen.
This changeset introduces a way to see each available translation update on the Updates screen, and allows users to update them selectively. With this change, translation updates management is more consistent with how plugin and theme updates are managed.
Developed in https://github.com/WordPress/wordpress-develop/pull/11610.
Props stk_jj, Presskopp, jyria, tfrommen, audrasjb, ozgursar, im3dabasia1.
Fixes #42281.
Built from https://develop.svn.wordpress.org/trunk@65226
git-svn-id: http://core.svn.wordpress.org/trunk@64375 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-admin/includes/class-language-pack-upgrader.php b/wp-admin/includes/class-language-pack-upgrader.php
index 43f211bbd0..59a144ed4a 100644
--- a/wp-admin/includes/class-language-pack-upgrader.php
+++ b/wp-admin/includes/class-language-pack-upgrader.php
@@ -68,9 +68,15 @@ class Language_Pack_Upgrader extends WP_Upgrader {
return;
}
+ $deferred_updates = wp_get_deferred_translation_updates( $language_updates );
+
foreach ( $language_updates as $key => $language_update ) {
$update = ! empty( $language_update->autoupdate );
+ if ( $update && wp_is_translation_update_deferred( $language_update, $deferred_updates ) ) {
+ $update = false;
+ }
+
/**
* Filters whether to asynchronously update translation for core, a plugin, or a theme.
*
@@ -230,6 +236,7 @@ class Language_Pack_Upgrader extends WP_Upgrader {
}
$language_updates_results = array();
+ $successful_update_ids = array();
foreach ( $language_updates as $language_update ) {
@@ -266,6 +273,10 @@ class Language_Pack_Upgrader extends WP_Upgrader {
break;
}
+ if ( is_array( $result ) ) {
+ $successful_update_ids[ wp_get_translation_update_id( $language_update ) ] = true;
+ }
+
$language_updates_results[] = array(
'language' => $language_update->language,
'type' => $language_update->type,
@@ -274,6 +285,15 @@ class Language_Pack_Upgrader extends WP_Upgrader {
);
}
+ if ( $successful_update_ids ) {
+ $deferred_updates = wp_get_deferred_translation_updates();
+ $remaining = array_diff_key( $deferred_updates, $successful_update_ids );
+
+ if ( $remaining !== $deferred_updates ) {
+ wp_set_deferred_translation_updates( $remaining );
+ }
+ }
+
// Remove upgrade hooks which are not required for translation updates.
remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
remove_action( 'upgrader_process_complete', 'wp_version_check' );
diff --git a/wp-admin/includes/class-wp-automatic-updater.php b/wp-admin/includes/class-wp-automatic-updater.php
index 0bb8937d0f..685719efd9 100644
--- a/wp-admin/includes/class-wp-automatic-updater.php
+++ b/wp-admin/includes/class-wp-automatic-updater.php
@@ -312,6 +312,11 @@ class WP_Automatic_Updater {
}
} else {
$update = ! empty( $item->autoupdate );
+
+ if ( $update && wp_is_translation_update_deferred( $item ) ) {
+ $update = false;
+ }
+
}
// If the `disable_autoupdate` flag is set, override any user-choice, but allow filters.
diff --git a/wp-admin/includes/update.php b/wp-admin/includes/update.php
index c3c15c8fe3..22f44a33db 100644
--- a/wp-admin/includes/update.php
+++ b/wp-admin/includes/update.php
@@ -641,6 +641,211 @@ function get_theme_updates() {
return $update_themes;
}
+/**
+ * Gets plugin names for translation updates.
+ *
+ * @since 7.2.0
+ *
+ * @return string[] Plugin names keyed by plugin file, directory, basename, and slug.
+ */
+function wp_get_translation_update_plugin_names() {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+
+ $plugin_names = array();
+ $all_plugins = get_plugins();
+ $plugin_update = get_site_transient( 'update_plugins' );
+
+ if ( is_object( $plugin_update ) ) {
+ $plugin_updates = array();
+
+ if ( ! empty( $plugin_update->response ) && is_array( $plugin_update->response ) ) {
+ $plugin_updates = array_merge( $plugin_updates, $plugin_update->response );
+ }
+
+ if ( ! empty( $plugin_update->no_update ) && is_array( $plugin_update->no_update ) ) {
+ $plugin_updates = array_merge( $plugin_updates, $plugin_update->no_update );
+ }
+
+ foreach ( $plugin_updates as $plugin_file => $plugin_data ) {
+ $plugin_data = (object) $plugin_data;
+
+ if ( ! empty( $plugin_data->slug ) && ! empty( $all_plugins[ $plugin_file ]['Name'] ) ) {
+ $plugin_names[ $plugin_data->slug ] = $all_plugins[ $plugin_file ]['Name'];
+ }
+ }
+ }
+
+ foreach ( $all_plugins as $plugin_file => $plugin_data ) {
+ $plugin_basename = dirname( $plugin_file );
+
+ if ( isset( $plugin_data['Name'] ) ) {
+ $plugin_names[ $plugin_file ] = $plugin_data['Name'];
+ $plugin_names[ basename( $plugin_file, '.php' ) ] = $plugin_data['Name'];
+
+ if ( '.' !== $plugin_basename ) {
+ $plugin_names[ $plugin_basename ] = $plugin_data['Name'];
+ }
+ }
+ }
+
+ return $plugin_names;
+}
+
+/**
+ * Gets the display name for a translation update.
+ *
+ * @since 7.2.0
+ *
+ * @param object $update Translation update object.
+ * @param string[]|null $plugin_names Optional. Plugin names keyed by plugin file, directory,
+ * basename, and slug. Default null.
+ * @return string The translation update name.
+ */
+function wp_get_translation_update_name( $update, $plugin_names = null ) {
+ $type = isset( $update->type ) ? $update->type : '';
+ $slug = isset( $update->slug ) ? $update->slug : '';
+
+ switch ( $type ) {
+ case 'core':
+ return 'WordPress'; // Not translated.
+
+ case 'theme':
+ $theme = wp_get_theme( $slug );
+ if ( $theme->exists() ) {
+ return $theme->get( 'Name' );
+ }
+ break;
+
+ case 'plugin':
+ if ( null === $plugin_names ) {
+ $plugin_names = wp_get_translation_update_plugin_names();
+ }
+
+ if ( ! empty( $plugin_names[ $slug ] ) ) {
+ return $plugin_names[ $slug ];
+ }
+ break;
+ }
+
+ return $slug;
+}
+
+/**
+ * Gets the display language for a translation update.
+ *
+ * @since 7.2.0
+ *
+ * @param string $locale Translation update locale.
+ * @return string The translation update language.
+ */
+function wp_get_translation_update_language( $locale ) {
+ $translations = get_site_transient( 'available_translations' );
+
+ if ( is_array( $translations ) && ! empty( $translations[ $locale ]['native_name'] ) ) {
+ return sprintf(
+ /* translators: 1: Native language name, 2: Locale. */
+ __( '%1$s (%2$s)' ),
+ $translations[ $locale ]['native_name'],
+ $locale
+ );
+ }
+
+ return $locale;
+}
+
+/**
+ * Gets display data for available translation updates.
+ *
+ * @since 7.2.0
+ *
+ * @return array[] {
+ * An array of translation update display data.
+ *
+ * @type bool $checked Whether the translation update is selected for installation.
+ * @type bool $deferred Whether the translation update has been deferred.
+ * @type string $id Translation update identifier.
+ * @type string $language Translation update language.
+ * @type string $language_code Translation update locale.
+ * @type string $name Translation update name.
+ * @type string $slug Translation update slug.
+ * @type string $type Translation update type.
+ * @type string $version Translation update version.
+ * }
+ */
+function wp_get_translation_update_data() {
+ $available_updates = wp_get_translation_updates();
+ $deferred_translation_updates = wp_get_deferred_translation_updates( $available_updates );
+ $plugin_names = null;
+ $translation_updates = array();
+
+ foreach ( $available_updates as $update ) {
+ $translation_update_id = wp_get_translation_update_id( $update );
+ $language = isset( $update->language ) ? $update->language : '';
+ $type = isset( $update->type ) ? $update->type : '';
+ $deferred = isset( $deferred_translation_updates[ $translation_update_id ] );
+
+ if ( 'plugin' === $type && null === $plugin_names ) {
+ $plugin_names = wp_get_translation_update_plugin_names();
+ }
+
+ $translation_updates[] = array(
+ 'checked' => ! $deferred,
+ 'deferred' => $deferred,
+ 'id' => $translation_update_id,
+ 'language' => wp_get_translation_update_language( $language ),
+ 'language_code' => $language,
+ 'name' => wp_get_translation_update_name( $update, $plugin_names ),
+ 'slug' => isset( $update->slug ) ? $update->slug : '',
+ 'type' => $type,
+ 'version' => isset( $update->version ) ? $update->version : '',
+ );
+ }
+
+ return $translation_updates;
+}
+
+/**
+ * Processes a translation update selection and stores the deferred updates.
+ *
+ * Only currently available updates included in the submitted form can be selected
+ * or deferred. Previously deferred updates outside the form remain deferred, while
+ * newly available updates outside the form are left unchanged.
+ *
+ * The caller must check capabilities and the nonce before calling this function.
+ *
+ * @since 7.2.0
+ *
+ * @param string[] $translation_update_ids Sanitized identifiers of updates included in the form.
+ * @param string[] $selected_translation_update_ids Sanitized identifiers of selected updates.
+ * @return object[]|false Selected current translation updates keyed by identifier, an empty array
+ * if all updates in the form were deferred, or false if the form contains
+ * no currently available updates. On false, stored deferrals are unchanged.
+ */
+function wp_process_translation_update_selection( $translation_update_ids, $selected_translation_update_ids ) {
+ if ( empty( $translation_update_ids ) ) {
+ return false;
+ }
+
+ $current_translation_updates = wp_get_translation_updates_by_id();
+ $translation_updates = array_intersect_key( $current_translation_updates, array_flip( $translation_update_ids ) );
+
+ if ( empty( $translation_updates ) ) {
+ return false;
+ }
+
+ $selected_translation_updates = array_intersect_key( $translation_updates, array_flip( $selected_translation_update_ids ) );
+ $deferred_translation_updates = array_intersect_key(
+ $current_translation_updates,
+ wp_get_deferred_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 );
+
+ return $selected_translation_updates;
+}
+
/**
* Adds a callback to display update information for themes with updates available.
*
diff --git a/wp-admin/update-core.php b/wp-admin/update-core.php
index f1cd0c0a66..90e4d4aa60 100644
--- a/wp-admin/update-core.php
+++ b/wp-admin/update-core.php
@@ -813,8 +813,8 @@ function list_theme_updates() {
* @since 3.7.0
*/
function list_translation_updates() {
- $updates = wp_get_translation_updates();
- if ( ! $updates ) {
+ $translation_updates = wp_get_translation_update_data();
+ if ( ! $translation_updates ) {
if ( 'en_US' !== get_locale() ) {
echo '<h2>' . __( 'Translations' ) . '</h2>';
echo '<p>' . __( 'Your translations are all up to date.' ) . '</p>';
@@ -822,13 +822,79 @@ function list_translation_updates() {
return;
}
- $form_action = 'update-core.php?action=do-translation-upgrade';
+ $form_action = 'update-core.php?action=do-translation-upgrade';
+ $updates_count = count( $translation_updates );
?>
- <h2><?php _e( 'Translations' ); ?></h2>
+ <h2>
+ <?php
+ printf(
+ '%s <span class="count">(%s)</span>',
+ __( 'Translations' ),
+ number_format_i18n( $updates_count )
+ );
+ ?>
+ </h2>
<form method="post" action="<?php echo esc_url( $form_action ); ?>" name="upgrade-translations" class="upgrade">
- <p><?php _e( 'New translations are available.' ); ?></p>
+ <p><?php _e( 'The following translation updates are available. Leave any translation updates unchecked if you want to install them later, then click “Update Translations”.' ); ?></p>
+ <p><?php _e( 'Translation updates you leave unchecked will remain available until you select them.' ); ?></p>
<?php wp_nonce_field( 'upgrade-translations' ); ?>
- <p><input class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
+ <p><input id="upgrade-translations" class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
+ <table class="widefat updates-table" id="update-translations-table">
+ <thead>
+ <tr>
+ <td class="manage-column check-column"><input type="checkbox" id="translations-select-all" /></td>
+ <td class="manage-column"><label for="translations-select-all"><?php _e( 'Select All' ); ?></label></td>
+ </tr>
+ </thead>
+
+ <tbody class="plugins">
+ <?php foreach ( $translation_updates as $translation_update ) : ?>
+ <?php $checkbox_id = 'checkbox_' . md5( $translation_update['id'] ); ?>
+ <tr>
+ <td class="check-column">
+ <input type="hidden" name="translations[]" value="<?php echo esc_attr( $translation_update['id'] ); ?>" />
+ <input type="checkbox" name="checked[]" id="<?php echo esc_attr( $checkbox_id ); ?>" value="<?php echo esc_attr( $translation_update['id'] ); ?>" <?php checked( $translation_update['checked'] ); ?> />
+ <label for="<?php echo esc_attr( $checkbox_id ); ?>">
+ <span class="screen-reader-text">
+ <?php
+ printf(
+ /* translators: 1: Project name, 2: Language name or locale. */
+ esc_html__( 'Select the translation update for %1$s in %2$s' ),
+ esc_html( $translation_update['name'] ),
+ esc_html( $translation_update['language'] )
+ );
+ ?>
+ </span>
+ </label>
+ </td>
+ <td class="plugin-title"><p>
+ <strong><?php echo esc_html( $translation_update['name'] ); ?></strong>
+ <?php
+ printf(
+ /* translators: 1: Language name or locale, 2: Version number. */
+ esc_html__( '%1$s translation is available for version %2$s.' ),
+ esc_html( $translation_update['language'] ),
+ esc_html( $translation_update['version'] )
+ );
+
+ if ( $translation_update['deferred'] ) {
+ echo ' ';
+ esc_html_e( 'This translation update will remain available until you select it.' );
+ }
+ ?>
+ </p></td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+
+ <tfoot>
+ <tr>
+ <td class="manage-column check-column"><input type="checkbox" id="translations-select-all-2" /></td>
+ <td class="manage-column"><label for="translations-select-all-2"><?php _e( 'Select All' ); ?></label></td>
+ </tr>
+ </tfoot>
+ </table>
+ <p><input id="upgrade-translations-2" class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
</form>
<?php
}
@@ -1002,7 +1068,7 @@ $updates_howto = '<p>' . __( '<strong>WordPress</strong> — Updating your
$updates_howto .= '<p>' . __( '<strong>Themes and Plugins</strong> — To update individual themes or plugins from this screen, use the checkboxes to make your selection, then <strong>click on the appropriate “Update” button</strong>. To update all of your themes or plugins at once, you can check the box at the top of the section to select all before clicking the update button.' ) . '</p>';
if ( 'en_US' !== get_locale() ) {
- $updates_howto .= '<p>' . __( '<strong>Translations</strong> — 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 <strong>click the “Update Translations”</strong> button.' ) . '</p>';
+ $updates_howto .= '<p>' . __( '<strong>Translations</strong> — Translation updates are selected for installation by default. Leave any translation updates unchecked if you want to install them later, then <strong>click the “Update Translations”</strong> button. Translation updates you leave unchecked will remain available until you select them.' ) . '</p>';
}
get_current_screen()->add_help_tab(
@@ -1092,6 +1158,22 @@ if ( 'upgrade-core' === $action ) {
}
}
+ $translation_updates_status = isset( $_GET['translation_updates'] ) ? wp_unslash( $_GET['translation_updates'] ) : '';
+ if ( is_string( $translation_updates_status ) ) {
+ $translation_updates_status = sanitize_key( $translation_updates_status );
+ } else {
+ $translation_updates_status = '';
+ }
+
+ if ( 'deferred' === $translation_updates_status ) {
+ 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' );
@@ -1275,6 +1357,32 @@ if ( 'upgrade-core' === $action ) {
check_admin_referer( 'upgrade-translations' );
+ // Requests without the form's update identifiers must not install all translations.
+ $translation_update_ids = array_unique(
+ array_map(
+ 'sanitize_text_field',
+ wp_unslash( (array) ( $_POST['translations'] ?? array() ) )
+ )
+ );
+ $selected_translation_update_ids = array_unique(
+ array_map(
+ 'sanitize_text_field',
+ wp_unslash( (array) ( $_POST['checked'] ?? array() ) )
+ )
+ );
+
+ $selected_translation_updates = wp_process_translation_update_selection( $translation_update_ids, $selected_translation_update_ids );
+
+ if ( false === $selected_translation_updates ) {
+ wp_redirect( self_admin_url( 'update-core.php' ) );
+ exit;
+ }
+
+ 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';
@@ -1284,7 +1392,7 @@ if ( 'upgrade-core' === $action ) {
$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/wp-includes/update.php b/wp-includes/update.php
index 9f4257ab03..b70220b790 100644
--- a/wp-includes/update.php
+++ b/wp-includes/update.php
@@ -920,6 +920,145 @@ function wp_get_translation_updates() {
return $updates;
}
+/**
+ * Gets a stable identifier for a translation update.
+ *
+ * @since 7.2.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.2.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 updates remain available but are skipped by background updates by default.
+ * Explicit updates, including WP-CLI, can still install them. Successful installation
+ * through Language_Pack_Upgrader clears the matching deferral; failure leaves it unchanged.
+ *
+ * @since 7.2.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.2.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.2.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/wp-includes/version.php b/wp-includes/version.php
index 8216741297..24716ce7b6 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta3-65225';
+$wp_version = '7.2-beta3-65226';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.