Make WordPress Core

Changeset 64232


Ignore:
Timestamp:
10/07/2026 05:33:55 PM (3 hours ago)
Author:
westonruter
Message:

I18N: Improve PHPStan types for the localization API.

Translatable strings, contexts, and text domains are extracted statically from source, so the gettext functions now require literal strings for these arguments. This lets PHPStan flag values that can never be translated, even when they reach the function indirectly through a variable or a nooped plural array. The return type of these functions remains string, since an empty string can still result from the gettext filters or from an empty translation in a translation file.

Correcting the documented types of the $l10n and $l10n_unloaded globals revealed that the return type of get_translations_for_domain() has been inaccurate since r57337, because WP_Translations does not extend Translations. The $l10n type retains MO since plugins may still assign such instances to the global directly, which load_textdomain() accounts for.

Making the declared header shapes hold for wp_get_pomo_file_data() and wp_get_l10n_php_file_data() required small runtime changes: the former no longer relies on preg_replace(), which may return null, and the latter now ignores non-string header values from a malformed translation file.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/14079.
Follow-up to r25520, r38198, r55279, r57337, r58061.

Props westonruter, swissspidy.
See #65817.

Location:
trunk/src/wp-includes
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-locale.php

    r61637 r64232  
    443443         * @return string Localized word count type. Possible values are `characters_excluding_spaces`,
    444444         *                `characters_including_spaces`, or `words`. Defaults to `words`.
     445         *
     446         * @phpstan-return 'characters_excluding_spaces'|'characters_including_spaces'|'words'
    445447         */
    446448        public function get_word_count_type() {
  • trunk/src/wp-includes/l10n.php

    r64163 r64232  
    333333 *                       Default 'default'.
    334334 * @return string Translated text.
     335 *
     336 * @phpstan-param literal-string $text
     337 * @phpstan-param literal-string $domain
    335338 */
    336339function __( $text, $domain = 'default' ) {
    … …  
    349352 *                       Default 'default'.
    350353 * @return string Translated text on success, original text on failure.
     354 *
     355 * @phpstan-param literal-string $text
     356 * @phpstan-param literal-string $domain
    351357 */
    352358function esc_attr__( $text, $domain = 'default' ) {
    … …  
    366372 *                       Default 'default'.
    367373 * @return string Translated text.
     374 *
     375 * @phpstan-param literal-string $text
     376 * @phpstan-param literal-string $domain
    368377 */
    369378function esc_html__( $text, $domain = 'default' ) {
    … …  
    379388 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
    380389 *                       Default 'default'.
     390 *
     391 * @phpstan-param literal-string $text
     392 * @phpstan-param literal-string $domain
    381393 */
    382394function _e( $text, $domain = 'default' ) {
    … …  
    397409 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
    398410 *                       Default 'default'.
     411 *
     412 * @phpstan-param literal-string $text
     413 * @phpstan-param literal-string $domain
    399414 */
    400415function esc_attr_e( $text, $domain = 'default' ) {
    … …  
    415430 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
    416431 *                       Default 'default'.
     432 *
     433 * @phpstan-param literal-string $text
     434 * @phpstan-param literal-string $domain
    417435 */
    418436function esc_html_e( $text, $domain = 'default' ) {
    … …  
    436454 *                        Default 'default'.
    437455 * @return string Translated context string without pipe.
     456 *
     457 * @phpstan-param literal-string $text
     458 * @phpstan-param literal-string $context
     459 * @phpstan-param literal-string $domain
    438460 */
    439461function _x( $text, $context, $domain = 'default' ) {
    … …  
    450472 * @param string $domain  Optional. Text domain. Unique identifier for retrieving translated strings.
    451473 *                        Default 'default'.
     474 *
     475 * @phpstan-param literal-string $text
     476 * @phpstan-param literal-string $context
     477 * @phpstan-param literal-string $domain
    452478 */
    453479function _ex( $text, $context, $domain = 'default' ) {
    … …  
    468494 *                        Default 'default'.
    469495 * @return string Translated text.
     496 *
     497 * @phpstan-param literal-string $text
     498 * @phpstan-param literal-string $context
     499 * @phpstan-param literal-string $domain
    470500 */
    471501function esc_attr_x( $text, $context, $domain = 'default' ) {
    … …  
    486516 *                        Default 'default'.
    487517 * @return string Translated text.
     518 *
     519 * @phpstan-param literal-string $text
     520 * @phpstan-param literal-string $context
     521 * @phpstan-param literal-string $domain
    488522 */
    489523function esc_html_x( $text, $context, $domain = 'default' ) {
    … …  
    510544 *                       Default 'default'.
    511545 * @return string The translated singular or plural form.
     546 *
     547 * @phpstan-param literal-string $single
     548 * @phpstan-param literal-string $plural
     549 * @phpstan-param literal-string $domain
    512550 */
    513551function _n( $single, $plural, $number, $domain = 'default' ) {
    … …  
    569607 *                        Default 'default'.
    570608 * @return string The translated singular or plural form.
     609 *
     610 * @phpstan-param literal-string $single
     611 * @phpstan-param literal-string $plural
     612 * @phpstan-param literal-string $context
     613 * @phpstan-param literal-string $domain
    571614 */
    572615function _nx( $single, $plural, $number, $context, $domain = 'default' ) {
    … …  
    634677 *     @type null        $context  Context information for the translators.
    635678 *     @type string|null $domain   Text domain.
     679 * }
     680 *
     681 * @phpstan-param literal-string $singular
     682 * @phpstan-param literal-string $plural
     683 * @phpstan-param literal-string|null $domain
     684 * @phpstan-return array{
     685 *     0: literal-string,
     686 *     1: literal-string,
     687 *     singular: literal-string,
     688 *     plural: literal-string,
     689 *     context: null,
     690 *     domain: literal-string|null,
    636691 * }
    637692 */
    … …  
    680735 *     @type string      $context  Context information for the translators.
    681736 *     @type string|null $domain   Text domain.
     737 * }
     738 *
     739 * @phpstan-param literal-string $singular
     740 * @phpstan-param literal-string $plural
     741 * @phpstan-param literal-string $context
     742 * @phpstan-param literal-string|null $domain
     743 * @phpstan-return array{
     744 *     0: literal-string,
     745 *     1: literal-string,
     746 *     2: literal-string,
     747 *     singular: literal-string,
     748 *     plural: literal-string,
     749 *     context: literal-string,
     750 *     domain: literal-string|null,
    682751 * }
    683752 */
    … …  
    720789 *                              a text domain passed to _n_noop() or _nx_noop(), it will override this value. Default 'default'.
    721790 * @return string Either $singular or $plural translated text.
     791 *
     792 * @phpstan-param array{
     793 *     singular: literal-string,
     794 *     plural: literal-string,
     795 *     context: literal-string|null,
     796 *     domain: literal-string|null,
     797 *     ...
     798 * } $nooped_plural
     799 * @phpstan-param literal-string $domain
    722800 */
    723801function translate_nooped_plural( $nooped_plural, $count, $domain = 'default' ) {
    … …  
    745823 * @since 6.1.0 Added the `$locale` parameter.
    746824 *
    747  * @global MO[]                  $l10n                   An array of all currently loaded text domains.
    748  * @global MO[]                   $l10n_unloaded          An array of all text domains that have been unloaded again.
    749  * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
     825 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n                   An array of all currently loaded text domains.
     826 * @global array<string, true>                                 $l10n_unloaded          An array of all text domains that have been unloaded again.
     827 * @global WP_Textdomain_Registry                              $wp_textdomain_registry WordPress Textdomain Registry.
    750828 *
    751829 * @param string $domain Text domain. Unique identifier for retrieving translated strings.
    … …  
    755833 */
    756834function load_textdomain( $domain, $mofile, $locale = null ) {
    757         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    758835        global $l10n, $l10n_unloaded, $wp_textdomain_registry;
    759836
    … …  
    900977 * @since 6.1.0 Added the `$reloadable` parameter.
    901978 *
    902  * @global MO[] $l10n          An array of all currently loaded text domains.
    903  * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
     979 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n          An array of all currently loaded text domains.
     980 * @global array<string, true>                                $l10n_unloaded An array of all text domains that have been unloaded again.
    904981 *
    905982 * @param string $domain     Text domain. Unique identifier for retrieving translated strings.
    … …  
    10171094 * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism.
    10181095 *
    1019  * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
    1020  * @global array<string, WP_Translations|NOOP_Translations> $l10n An array of all currently loaded text domains.
     1096 * @global WP_Textdomain_Registry                              $wp_textdomain_registry WordPress Textdomain Registry.
     1097 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n                  An array of all currently loaded text domains.
    10211098 *
    10221099 * @param string       $domain          Unique identifier for retrieving translated strings
    … …  
    10301107 */
    10311108function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
    1032         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    1033         /** @var array<string, WP_Translations|NOOP_Translations> $l10n */
    10341109        global $wp_textdomain_registry, $l10n;
    10351110
    … …  
    10641139 * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism.
    10651140 *
    1066  * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
    1067  * @global array<string, WP_Translations|NOOP_Translations> $l10n An array of all currently loaded text domains.
     1141 * @global WP_Textdomain_Registry                              $wp_textdomain_registry WordPress Textdomain Registry.
     1142 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n                  An array of all currently loaded text domains.
    10681143 *
    10691144 * @param string $domain             Text domain. Unique identifier for retrieving translated strings.
    … …  
    10731148 */
    10741149function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
    1075         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    1076         /** @var array<string, WP_Translations|NOOP_Translations> $l10n */
    10771150        global $wp_textdomain_registry, $l10n;
    10781151
    … …  
    11051178 * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism.
    11061179 *
    1107  * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
    1108  * @global array<string, WP_Translations|NOOP_Translations> $l10n An array of all currently loaded text domains.
     1180 * @global WP_Textdomain_Registry                              $wp_textdomain_registry WordPress Textdomain Registry.
     1181 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n                  An array of all currently loaded text domains.
    11091182 *
    11101183 * @param string       $domain Text domain. Unique identifier for retrieving translated strings.
    … …  
    11141187 */
    11151188function load_theme_textdomain( $domain, $path = false ) {
    1116         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    1117         /** @var array<string, WP_Translations|NOOP_Translations> $l10n */
    11181189        global $wp_textdomain_registry, $l10n;
    11191190
    … …  
    12391310 */
    12401311function _load_script_textdomain_from_src( string $handle, string $src, string $domain, string $path, bool $is_module ) {
    1241         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    12421312        global $wp_textdomain_registry;
    12431313
    … …  
    14471517 * @access private
    14481518 *
    1449  * @global MO[]                   $l10n_unloaded          An array of all text domains that have been unloaded again.
     1519 * @global array<string, true>    $l10n_unloaded          An array of all text domains that have been unloaded again.
    14501520 * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
    14511521 *
    … …  
    14541524 */
    14551525function _load_textdomain_just_in_time( $domain ) {
    1456         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    14571526        global $l10n_unloaded, $wp_textdomain_registry;
    14581527
    … …  
    15061575 * @since 2.8.0
    15071576 *
    1508  * @global MO[] $l10n An array of all currently loaded text domains.
     1577 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n An array of all currently loaded text domains.
    15091578 *
    15101579 * @param string $domain Text domain. Unique identifier for retrieving translated strings.
    1511  * @return Translations|NOOP_Translations A Translations instance.
     1580 * @return WP_Translations|Translations|NOOP_Translations A Translations instance.
    15121581 */
    15131582function get_translations_for_domain( $domain ) {
    … …  
    15321601 * @since 3.0.0
    15331602 *
    1534  * @global MO[] $l10n An array of all currently loaded text domains.
     1603 * @global array<string, WP_Translations|NOOP_Translations|MO> $l10n An array of all currently loaded text domains.
    15351604 *
    15361605 * @param string $domain Text domain. Unique identifier for retrieving translated strings.
    … …  
    16241693 *
    16251694 * @param string $type What to search for. Accepts 'plugins', 'themes', 'core'.
    1626  * @return array Array of language data.
     1695 * @return array<string, array<string, array<string, string>>> Array of language data, keyed by text domain
     1696 *                                                             and then by locale, each value being the
     1697 *                                                             translation file headers.
     1698 *
     1699 * @phpstan-return (
     1700 *     $type is 'plugins'|'themes'|'core'
     1701 *         ? array<string, array<string, array{
     1702 *             'POT-Creation-Date': string,
     1703 *             'PO-Revision-Date': string,
     1704 *             'Project-Id-Version': string,
     1705 *             'X-Generator': string,
     1706 *         }>>
     1707 *         : array{}
     1708 * )
    16271709 */
    16281710function wp_get_installed_translations( $type ) {
    … …  
    16841766 *
    16851767 * @param string $po_file Path to PO file.
    1686  * @return string[] Array of PO file header values keyed by header name.
     1768 * @return array<string, string> Array of PO file header values keyed by header name.
     1769 *
     1770 * @phpstan-return array{
     1771 *     'POT-Creation-Date': string,
     1772 *     'PO-Revision-Date': string,
     1773 *     'Project-Id-Version': string,
     1774 *     'X-Generator': string,
     1775 * }
    16871776 */
    16881777function wp_get_pomo_file_data( $po_file ) {
    … …  
    16961785                )
    16971786        );
    1698         foreach ( $headers as $header => $value ) {
    1699                 // Remove possible contextual '\n' and closing double quote.
    1700                 $headers[ $header ] = preg_replace( '~(\\\n)?"$~', '', $value );
    1701         }
    1702         return $headers;
     1787
     1788        $result = array(
     1789                'POT-Creation-Date'  => '',
     1790                'PO-Revision-Date'   => '',
     1791                'Project-Id-Version' => '',
     1792                'X-Generator'        => '',
     1793        );
     1794
     1795        foreach ( array_keys( $result ) as $header ) {
     1796                $value = $headers[ $header ];
     1797
     1798                // Remove possible closing double quote and the contextual '\n' preceding it.
     1799                if ( str_ends_with( $value, '"' ) ) {
     1800                        $value = substr( $value, 0, -1 );
     1801                        if ( str_ends_with( $value, '\n' ) ) {
     1802                                $value = substr( $value, 0, -2 );
     1803                        }
     1804                }
     1805
     1806                $result[ $header ] = $value;
     1807        }
     1808
     1809        return $result;
    17031810}
    17041811
    … …  
    17091816 *
    17101817 * @param string $php_file Path to a `.l10n.php` file.
    1711  * @return string[] Array of file header values keyed by header name.
     1818 * @return array<string, string> Array of file header values keyed by header name.
     1819 *
     1820 * @phpstan-return array{
     1821 *     'POT-Creation-Date': string,
     1822 *     'PO-Revision-Date': string,
     1823 *     'Project-Id-Version': string,
     1824 *     'X-Generator': string,
     1825 * }
    17121826 */
    17131827function wp_get_l10n_php_file_data( $php_file ) {
    … …  
    17301844
    17311845        foreach ( $headers as $po_header => $php_header ) {
    1732                 if ( isset( $data[ $php_header ] ) ) {
     1846                if ( isset( $data[ $php_header ] ) && is_string( $data[ $php_header ] ) ) {
    17331847                        $result[ $po_header ] = $data[ $php_header ];
    17341848                }
    … …  
    20992213 * @return string Locale-specific word count type. Possible values are `characters_excluding_spaces`,
    21002214 *                `characters_including_spaces`, or `words`. Defaults to `words`.
     2215 *
     2216 * @phpstan-return 'characters_excluding_spaces'|'characters_including_spaces'|'words'
    21012217 */
    21022218function wp_get_word_count_type() {
Note: See TracChangeset for help on using the changeset viewer.