Make WordPress Core

Changeset 65226


Ignore:
Timestamp:
11/03/2026 01:38:37 PM (9 days ago)
Author:
audrasjb
Message:

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.

Location:
trunk
Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-language-pack-upgrader.php

    r64435 r65226  
    6969                }
    7070
     71                $deferred_updates = wp_get_deferred_translation_updates( $language_updates );
     72
    7173                foreach ( $language_updates as $key => $language_update ) {
    7274                        $update = ! empty( $language_update->autoupdate );
     75
     76                        if ( $update && wp_is_translation_update_deferred( $language_update, $deferred_updates ) ) {
     77                                $update = false;
     78                        }
    7379
    7480                        /**
    … …  
    231237
    232238                $language_updates_results = array();
     239                $successful_update_ids    = array();
    233240
    234241                foreach ( $language_updates as $language_update ) {
    … …  
    267274                        }
    268275
     276                        if ( is_array( $result ) ) {
     277                                $successful_update_ids[ wp_get_translation_update_id( $language_update ) ] = true;
     278                        }
     279
    269280                        $language_updates_results[] = array(
    270281                                'language' => $language_update->language,
    … …  
    273284                                'version'  => $language_update->version,
    274285                        );
     286                }
     287
     288                if ( $successful_update_ids ) {
     289                        $deferred_updates = wp_get_deferred_translation_updates();
     290                        $remaining        = array_diff_key( $deferred_updates, $successful_update_ids );
     291
     292                        if ( $remaining !== $deferred_updates ) {
     293                                wp_set_deferred_translation_updates( $remaining );
     294                        }
    275295                }
    276296
  • trunk/src/wp-admin/includes/class-wp-automatic-updater.php

    r64582 r65226  
    313313                } else {
    314314                        $update = ! empty( $item->autoupdate );
     315
     316                        if ( $update && wp_is_translation_update_deferred( $item ) ) {
     317                                $update = false;
     318                        }
     319
    315320                }
    316321
  • trunk/src/wp-admin/includes/update.php

    r64586 r65226  
    643643
    644644/**
     645 * Gets plugin names for translation updates.
     646 *
     647 * @since 7.2.0
     648 *
     649 * @return string[] Plugin names keyed by plugin file, directory, basename, and slug.
     650 */
     651function wp_get_translation_update_plugin_names() {
     652        require_once ABSPATH . 'wp-admin/includes/plugin.php';
     653
     654        $plugin_names  = array();
     655        $all_plugins   = get_plugins();
     656        $plugin_update = get_site_transient( 'update_plugins' );
     657
     658        if ( is_object( $plugin_update ) ) {
     659                $plugin_updates = array();
     660
     661                if ( ! empty( $plugin_update->response ) && is_array( $plugin_update->response ) ) {
     662                        $plugin_updates = array_merge( $plugin_updates, $plugin_update->response );
     663                }
     664
     665                if ( ! empty( $plugin_update->no_update ) && is_array( $plugin_update->no_update ) ) {
     666                        $plugin_updates = array_merge( $plugin_updates, $plugin_update->no_update );
     667                }
     668
     669                foreach ( $plugin_updates as $plugin_file => $plugin_data ) {
     670                        $plugin_data = (object) $plugin_data;
     671
     672                        if ( ! empty( $plugin_data->slug ) && ! empty( $all_plugins[ $plugin_file ]['Name'] ) ) {
     673                                $plugin_names[ $plugin_data->slug ] = $all_plugins[ $plugin_file ]['Name'];
     674                        }
     675                }
     676        }
     677
     678        foreach ( $all_plugins as $plugin_file => $plugin_data ) {
     679                $plugin_basename = dirname( $plugin_file );
     680
     681                if ( isset( $plugin_data['Name'] ) ) {
     682                        $plugin_names[ $plugin_file ]                     = $plugin_data['Name'];
     683                        $plugin_names[ basename( $plugin_file, '.php' ) ] = $plugin_data['Name'];
     684
     685                        if ( '.' !== $plugin_basename ) {
     686                                $plugin_names[ $plugin_basename ] = $plugin_data['Name'];
     687                        }
     688                }
     689        }
     690
     691        return $plugin_names;
     692}
     693
     694/**
     695 * Gets the display name for a translation update.
     696 *
     697 * @since 7.2.0
     698 *
     699 * @param object        $update       Translation update object.
     700 * @param string[]|null $plugin_names Optional. Plugin names keyed by plugin file, directory,
     701 *                                    basename, and slug. Default null.
     702 * @return string The translation update name.
     703 */
     704function wp_get_translation_update_name( $update, $plugin_names = null ) {
     705        $type = isset( $update->type ) ? $update->type : '';
     706        $slug = isset( $update->slug ) ? $update->slug : '';
     707
     708        switch ( $type ) {
     709                case 'core':
     710                        return 'WordPress'; // Not translated.
     711
     712                case 'theme':
     713                        $theme = wp_get_theme( $slug );
     714                        if ( $theme->exists() ) {
     715                                return $theme->get( 'Name' );
     716                        }
     717                        break;
     718
     719                case 'plugin':
     720                        if ( null === $plugin_names ) {
     721                                $plugin_names = wp_get_translation_update_plugin_names();
     722                        }
     723
     724                        if ( ! empty( $plugin_names[ $slug ] ) ) {
     725                                return $plugin_names[ $slug ];
     726                        }
     727                        break;
     728        }
     729
     730        return $slug;
     731}
     732
     733/**
     734 * Gets the display language for a translation update.
     735 *
     736 * @since 7.2.0
     737 *
     738 * @param string $locale Translation update locale.
     739 * @return string The translation update language.
     740 */
     741function wp_get_translation_update_language( $locale ) {
     742        $translations = get_site_transient( 'available_translations' );
     743
     744        if ( is_array( $translations ) && ! empty( $translations[ $locale ]['native_name'] ) ) {
     745                return sprintf(
     746                        /* translators: 1: Native language name, 2: Locale. */
     747                        __( '%1$s (%2$s)' ),
     748                        $translations[ $locale ]['native_name'],
     749                        $locale
     750                );
     751        }
     752
     753        return $locale;
     754}
     755
     756/**
     757 * Gets display data for available translation updates.
     758 *
     759 * @since 7.2.0
     760 *
     761 * @return array[] {
     762 *     An array of translation update display data.
     763 *
     764 *     @type bool   $checked       Whether the translation update is selected for installation.
     765 *     @type bool   $deferred      Whether the translation update has been deferred.
     766 *     @type string $id            Translation update identifier.
     767 *     @type string $language      Translation update language.
     768 *     @type string $language_code Translation update locale.
     769 *     @type string $name          Translation update name.
     770 *     @type string $slug          Translation update slug.
     771 *     @type string $type          Translation update type.
     772 *     @type string $version       Translation update version.
     773 * }
     774 */
     775function wp_get_translation_update_data() {
     776        $available_updates            = wp_get_translation_updates();
     777        $deferred_translation_updates = wp_get_deferred_translation_updates( $available_updates );
     778        $plugin_names                 = null;
     779        $translation_updates          = array();
     780
     781        foreach ( $available_updates as $update ) {
     782                $translation_update_id = wp_get_translation_update_id( $update );
     783                $language              = isset( $update->language ) ? $update->language : '';
     784                $type                  = isset( $update->type ) ? $update->type : '';
     785                $deferred              = isset( $deferred_translation_updates[ $translation_update_id ] );
     786
     787                if ( 'plugin' === $type && null === $plugin_names ) {
     788                        $plugin_names = wp_get_translation_update_plugin_names();
     789                }
     790
     791                $translation_updates[] = array(
     792                        'checked'       => ! $deferred,
     793                        'deferred'      => $deferred,
     794                        'id'            => $translation_update_id,
     795                        'language'      => wp_get_translation_update_language( $language ),
     796                        'language_code' => $language,
     797                        'name'          => wp_get_translation_update_name( $update, $plugin_names ),
     798                        'slug'          => isset( $update->slug ) ? $update->slug : '',
     799                        'type'          => $type,
     800                        'version'       => isset( $update->version ) ? $update->version : '',
     801                );
     802        }
     803
     804        return $translation_updates;
     805}
     806
     807/**
     808 * Processes a translation update selection and stores the deferred updates.
     809 *
     810 * Only currently available updates included in the submitted form can be selected
     811 * or deferred. Previously deferred updates outside the form remain deferred, while
     812 * newly available updates outside the form are left unchanged.
     813 *
     814 * The caller must check capabilities and the nonce before calling this function.
     815 *
     816 * @since 7.2.0
     817 *
     818 * @param string[] $translation_update_ids          Sanitized identifiers of updates included in the form.
     819 * @param string[] $selected_translation_update_ids Sanitized identifiers of selected updates.
     820 * @return object[]|false Selected current translation updates keyed by identifier, an empty array
     821 *                        if all updates in the form were deferred, or false if the form contains
     822 *                        no currently available updates. On false, stored deferrals are unchanged.
     823 */
     824function wp_process_translation_update_selection( $translation_update_ids, $selected_translation_update_ids ) {
     825        if ( empty( $translation_update_ids ) ) {
     826                return false;
     827        }
     828
     829        $current_translation_updates = wp_get_translation_updates_by_id();
     830        $translation_updates         = array_intersect_key( $current_translation_updates, array_flip( $translation_update_ids ) );
     831
     832        if ( empty( $translation_updates ) ) {
     833                return false;
     834        }
     835
     836        $selected_translation_updates  = array_intersect_key( $translation_updates, array_flip( $selected_translation_update_ids ) );
     837        $deferred_translation_updates  = array_intersect_key(
     838                $current_translation_updates,
     839                wp_get_deferred_translation_updates()
     840        );
     841        $deferred_translation_updates  = array_diff_key( $deferred_translation_updates, $translation_updates );
     842        $deferred_translation_updates += array_diff_key( $translation_updates, $selected_translation_updates );
     843
     844        wp_set_deferred_translation_updates( $deferred_translation_updates );
     845
     846        return $selected_translation_updates;
     847}
     848
     849/**
    645850 * Adds a callback to display update information for themes with updates available.
    646851 *
  • trunk/src/wp-admin/update-core.php

    r61520 r65226  
    814814 */
    815815function list_translation_updates() {
    816         $updates = wp_get_translation_updates();
    817         if ( ! $updates ) {
     816        $translation_updates = wp_get_translation_update_data();
     817        if ( ! $translation_updates ) {
    818818                if ( 'en_US' !== get_locale() ) {
    819819                        echo '<h2>' . __( 'Translations' ) . '</h2>';
    … …  
    823823        }
    824824
    825         $form_action = 'update-core.php?action=do-translation-upgrade';
     825        $form_action   = 'update-core.php?action=do-translation-upgrade';
     826        $updates_count = count( $translation_updates );
    826827        ?>
    827         <h2><?php _e( 'Translations' ); ?></h2>
     828        <h2>
     829        <?php
     830        printf(
     831                '%s <span class="count">(%s)</span>',
     832                __( 'Translations' ),
     833                number_format_i18n( $updates_count )
     834        );
     835        ?>
     836        </h2>
    828837        <form method="post" action="<?php echo esc_url( $form_action ); ?>" name="upgrade-translations" class="upgrade">
    829                 <p><?php _e( 'New translations are available.' ); ?></p>
     838                <p><?php _e( 'The following translation updates are available. Leave any translation updates unchecked if you want to install them later, then click &#8220;Update Translations&#8221;.' ); ?></p>
     839                <p><?php _e( 'Translation updates you leave unchecked will remain available until you select them.' ); ?></p>
    830840                <?php wp_nonce_field( 'upgrade-translations' ); ?>
    831                 <p><input class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
     841                <p><input id="upgrade-translations" class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
     842                <table class="widefat updates-table" id="update-translations-table">
     843                        <thead>
     844                        <tr>
     845                                <td class="manage-column check-column"><input type="checkbox" id="translations-select-all" /></td>
     846                                <td class="manage-column"><label for="translations-select-all"><?php _e( 'Select All' ); ?></label></td>
     847                        </tr>
     848                        </thead>
     849
     850                        <tbody class="plugins">
     851                        <?php foreach ( $translation_updates as $translation_update ) : ?>
     852                                <?php $checkbox_id = 'checkbox_' . md5( $translation_update['id'] ); ?>
     853                                <tr>
     854                                        <td class="check-column">
     855                                                <input type="hidden" name="translations[]" value="<?php echo esc_attr( $translation_update['id'] ); ?>" />
     856                                                <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'] ); ?> />
     857                                                <label for="<?php echo esc_attr( $checkbox_id ); ?>">
     858                                                        <span class="screen-reader-text">
     859                                                        <?php
     860                                                        printf(
     861                                                                /* translators: 1: Project name, 2: Language name or locale. */
     862                                                                esc_html__( 'Select the translation update for %1$s in %2$s' ),
     863                                                                esc_html( $translation_update['name'] ),
     864                                                                esc_html( $translation_update['language'] )
     865                                                        );
     866                                                        ?>
     867                                                        </span>
     868                                                </label>
     869                                        </td>
     870                                        <td class="plugin-title"><p>
     871                                                <strong><?php echo esc_html( $translation_update['name'] ); ?></strong>
     872                                                <?php
     873                                                printf(
     874                                                        /* translators: 1: Language name or locale, 2: Version number. */
     875                                                        esc_html__( '%1$s translation is available for version %2$s.' ),
     876                                                        esc_html( $translation_update['language'] ),
     877                                                        esc_html( $translation_update['version'] )
     878                                                );
     879
     880                                                if ( $translation_update['deferred'] ) {
     881                                                        echo ' ';
     882                                                        esc_html_e( 'This translation update will remain available until you select it.' );
     883                                                }
     884                                                ?>
     885                                        </p></td>
     886                                </tr>
     887                        <?php endforeach; ?>
     888                        </tbody>
     889
     890                        <tfoot>
     891                        <tr>
     892                                <td class="manage-column check-column"><input type="checkbox" id="translations-select-all-2" /></td>
     893                                <td class="manage-column"><label for="translations-select-all-2"><?php _e( 'Select All' ); ?></label></td>
     894                        </tr>
     895                        </tfoot>
     896                </table>
     897                <p><input id="upgrade-translations-2" class="button" type="submit" value="<?php esc_attr_e( 'Update Translations' ); ?>" name="upgrade" /></p>
    832898        </form>
    833899        <?php
    … …  
    10031069
    10041070if ( 'en_US' !== get_locale() ) {
    1005         $updates_howto .= '<p>' . __( '<strong>Translations</strong> &mdash; 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 &#8220;Update Translations&#8221;</strong> button.' ) . '</p>';
     1071        $updates_howto .= '<p>' . __( '<strong>Translations</strong> &mdash; Translation updates are selected for installation by default. Leave any translation updates unchecked if you want to install them later, then <strong>click the &#8220;Update Translations&#8221;</strong> button. Translation updates you leave unchecked will remain available until you select them.' ) . '</p>';
    10061072}
    10071073
    … …  
    10931159        }
    10941160
     1161        $translation_updates_status = isset( $_GET['translation_updates'] ) ? wp_unslash( $_GET['translation_updates'] ) : '';
     1162        if ( is_string( $translation_updates_status ) ) {
     1163                $translation_updates_status = sanitize_key( $translation_updates_status );
     1164        } else {
     1165                $translation_updates_status = '';
     1166        }
     1167
     1168        if ( 'deferred' === $translation_updates_status ) {
     1169                wp_admin_notice(
     1170                        __( 'The unchecked translation updates will remain available until you select them.' ),
     1171                        array(
     1172                                'type' => 'success',
     1173                        )
     1174                );
     1175        }
     1176
    10951177        $last_update_check = false;
    10961178        $current           = get_site_transient( 'update_core' );
    … …  
    12761358        check_admin_referer( 'upgrade-translations' );
    12771359
     1360        // Requests without the form's update identifiers must not install all translations.
     1361        $translation_update_ids          = array_unique(
     1362                array_map(
     1363                        'sanitize_text_field',
     1364                        wp_unslash( (array) ( $_POST['translations'] ?? array() ) )
     1365                )
     1366        );
     1367        $selected_translation_update_ids = array_unique(
     1368                array_map(
     1369                        'sanitize_text_field',
     1370                        wp_unslash( (array) ( $_POST['checked'] ?? array() ) )
     1371                )
     1372        );
     1373
     1374        $selected_translation_updates = wp_process_translation_update_selection( $translation_update_ids, $selected_translation_update_ids );
     1375
     1376        if ( false === $selected_translation_updates ) {
     1377                wp_redirect( self_admin_url( 'update-core.php' ) );
     1378                exit;
     1379        }
     1380
     1381        if ( empty( $selected_translation_updates ) ) {
     1382                wp_redirect( add_query_arg( 'translation_updates', 'deferred', self_admin_url( 'update-core.php' ) ) );
     1383                exit;
     1384        }
     1385
    12781386        require_once ABSPATH . 'wp-admin/admin-header.php';
    12791387        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
    … …  
    12851393
    12861394        $upgrader = new Language_Pack_Upgrader( new Language_Pack_Upgrader_Skin( compact( 'url', 'nonce', 'title', 'context' ) ) );
    1287         $result   = $upgrader->bulk_upgrade();
     1395        $result   = $upgrader->bulk_upgrade( array_values( $selected_translation_updates ) );
    12881396
    12891397        wp_localize_script(
  • trunk/src/wp-includes/update.php

    r62649 r65226  
    922922
    923923/**
     924 * Gets a stable identifier for a translation update.
     925 *
     926 * @since 7.2.0
     927 *
     928 * @param array|object $update Translation update data.
     929 * @return string Translation update identifier.
     930 */
     931function wp_get_translation_update_id( $update ) {
     932        $update = (object) $update;
     933
     934        return md5(
     935                wp_json_encode(
     936                        array(
     937                                'type'     => $update->type ?? '',
     938                                'slug'     => $update->slug ?? '',
     939                                'language' => $update->language ?? '',
     940                                'version'  => $update->version ?? '',
     941                        )
     942                )
     943        );
     944}
     945
     946/**
     947 * Gets translation updates keyed by their stable identifiers.
     948 *
     949 * @since 7.2.0
     950 *
     951 * @param object[]|null $updates Optional. Translation update objects. Default null.
     952 * @return object[] Translation updates keyed by identifier.
     953 */
     954function wp_get_translation_updates_by_id( $updates = null ) {
     955        if ( null === $updates ) {
     956                $updates = wp_get_translation_updates();
     957        }
     958
     959        $translation_updates = array();
     960
     961        foreach ( (array) $updates as $update ) {
     962                $translation_updates[ wp_get_translation_update_id( $update ) ] = (object) $update;
     963        }
     964
     965        return $translation_updates;
     966}
     967
     968/**
     969 * Gets deferred translation updates.
     970 *
     971 * Deferred updates remain available but are skipped by background updates by default.
     972 * Explicit updates, including WP-CLI, can still install them. Successful installation
     973 * through Language_Pack_Upgrader clears the matching deferral; failure leaves it unchanged.
     974 *
     975 * @since 7.2.0
     976 *
     977 * @param object[]|null $updates Optional. Translation update objects used to filter deferred updates
     978 *                               to currently available updates. Default null.
     979 * @return array[] Deferred translation updates keyed by identifier.
     980 */
     981function wp_get_deferred_translation_updates( $updates = null ) {
     982        $stored_updates = get_site_option( 'deferred_translation_updates', array() );
     983
     984        if ( ! is_array( $stored_updates ) ) {
     985                return array();
     986        }
     987
     988        $deferred_updates = array();
     989
     990        foreach ( $stored_updates as $stored_update ) {
     991                if ( ! is_array( $stored_update ) ) {
     992                        continue;
     993                }
     994
     995                $stored_update = (object) array(
     996                        'type'     => $stored_update['type'] ?? '',
     997                        'slug'     => $stored_update['slug'] ?? '',
     998                        'language' => $stored_update['language'] ?? '',
     999                        'version'  => $stored_update['version'] ?? '',
     1000                );
     1001
     1002                $deferred_updates[ wp_get_translation_update_id( $stored_update ) ] = (array) $stored_update;
     1003        }
     1004
     1005        if ( null !== $updates ) {
     1006                $deferred_updates = array_intersect_key( $deferred_updates, wp_get_translation_updates_by_id( $updates ) );
     1007        }
     1008
     1009        return $deferred_updates;
     1010}
     1011
     1012/**
     1013 * Determines whether a translation update has been deferred.
     1014 *
     1015 * @since 7.2.0
     1016 *
     1017 * @param array|object $update           Translation update data.
     1018 * @param array[]|null $deferred_updates Optional. Deferred translation updates keyed by identifier.
     1019 *                                       Default null.
     1020 * @return bool Whether the translation update has been deferred.
     1021 */
     1022function wp_is_translation_update_deferred( $update, $deferred_updates = null ) {
     1023        if ( null === $deferred_updates ) {
     1024                $deferred_updates = wp_get_deferred_translation_updates();
     1025        }
     1026
     1027        return isset( $deferred_updates[ wp_get_translation_update_id( $update ) ] );
     1028}
     1029
     1030/**
     1031 * Stores deferred translation updates.
     1032 *
     1033 * @since 7.2.0
     1034 *
     1035 * @param object[]|array[] $updates Translation updates to defer.
     1036 * @return void
     1037 */
     1038function wp_set_deferred_translation_updates( $updates ) {
     1039        $deferred_updates = array();
     1040
     1041        foreach ( (array) $updates as $update ) {
     1042                $update = (object) $update;
     1043
     1044                $translation_update = array(
     1045                        'type'     => $update->type ?? '',
     1046                        'slug'     => $update->slug ?? '',
     1047                        'language' => $update->language ?? '',
     1048                        'version'  => $update->version ?? '',
     1049                );
     1050
     1051                $deferred_updates[ wp_get_translation_update_id( $translation_update ) ] = $translation_update;
     1052        }
     1053
     1054        if ( empty( $deferred_updates ) ) {
     1055                delete_site_option( 'deferred_translation_updates' );
     1056                return;
     1057        }
     1058
     1059        update_site_option( 'deferred_translation_updates', $deferred_updates );
     1060}
     1061
     1062/**
    9241063 * Collects counts and UI strings for available updates.
    9251064 *
  • trunk/tests/phpunit/tests/admin/wpAutomaticUpdater.php

    r65069 r65226  
    3939                parent::set_up();
    4040                add_filter( 'pre_wp_mail', '__return_false' );
     41        }
     42
     43        public function tear_down() {
     44                delete_site_option( 'deferred_translation_updates' );
     45
     46                parent::tear_down();
     47        }
     48
     49        /**
     50         * Tests that deferred translation updates are not automatically updated.
     51         *
     52         * @ticket 42281
     53         *
     54         * @covers WP_Automatic_Updater::should_update
     55         */
     56        public function test_should_update_should_skip_deferred_translation_updates() {
     57                $deferred_update = (object) array(
     58                        'type'       => 'plugin',
     59                        'slug'       => 'deferred-plugin',
     60                        'language'   => 'de_DE',
     61                        'version'    => '1.0.0',
     62                        'autoupdate' => true,
     63                );
     64
     65                $available_update       = clone $deferred_update;
     66                $available_update->slug = 'available-plugin';
     67
     68                wp_set_deferred_translation_updates( array( $deferred_update ) );
     69
     70                add_filter( 'request_filesystem_credentials', '__return_true' );
     71                add_filter( 'automatic_updates_is_vcs_checkout', '__return_false' );
     72                add_filter( 'automatic_updater_disabled', '__return_false' );
     73
     74                $this->assertFalse( self::$updater->should_update( 'translation', $deferred_update, WP_LANG_DIR ) );
     75                $this->assertTrue( self::$updater->should_update( 'translation', $available_update, WP_LANG_DIR ) );
    4176        }
    4277
Note: See TracChangeset for help on using the changeset viewer.