Skip to content

Conversation

Copy link

Copilot AI commented Nov 8, 2025

Description

wp language plugin list and wp language theme list incorrectly report all translations as "uninstalled" when a plugin or theme's directory slug differs from its text domain. This occurs because wp_get_installed_translations() keys results by text domain, but the code was checking using the slug.

Changes

  • Modified CommandWithTranslation::get_installed_languages() to read the TextDomain header from plugin data and theme metadata, using it instead of slug when checking installed translations
  • Falls back to slug when TextDomain is empty (preserves backward compatibility)
  • Added theme existence check before accessing theme data
  • Added test scenario for plugins with text domain different from slug
  • Added test scenario for themes with text domain different from slug

Example

Before:

// Plugin slug: my-plugin, TextDomain: different-domain
$available = wp_get_installed_translations('plugins');
// Returns: ['different-domain' => ['de_DE' => [...]]]
$installed = $available['my-plugin']; // Empty ❌

After:

$plugins = get_plugins('/my-plugin');
$plugin_data = array_shift($plugins);
$text_domain = $plugin_data['TextDomain'] ?? 'my-plugin';
$installed = $available[$text_domain]; // Correct ✅

Checklist

  • Functional tests included
  • Adheres to WordPress coding standards
  • No breaking changes

[!WARNING]

Original prompt

This section details on the original issue you should resolve

<filter_complete></filter_complete>

<issue_title>Incorrect language installation status when plugin's slug is different from its text domain</issue_title>
<issue_description>## Bug Report

Describe the current, buggy behavior

When listing available languages for a plugin using the wp language plugin list <plugin> command, the status is always uninstalled if the plugin's slug ≠ plugin's text domain.

This is due to the wp_get_installed_translations() method used in

protected function get_installed_languages( $slug = 'default' ) {
$available = wp_get_installed_translations( $this->obj_type );
$available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array();
$available[] = 'en_US';
return $available;
}

wp_get_installed_translations() returns an array keyed by the text domain. As a result, the subsequent check for available languages ($available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array();) results in an empty array if the plugin's slug is different from its text domain.

get_installed_languages() is used by the list command:

public function list_( $args, $assoc_args ) {
$all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false );
if ( ! $all && empty( $args ) ) {
WP_CLI::error( 'Please specify one or more plugins, or use --all.' );
}
if ( $all ) {
$args = array_map( '\WP_CLI\Utils\get_plugin_name', array_keys( $this->get_all_plugins() ) );
if ( empty( $args ) ) {
WP_CLI::success( 'No plugins installed.' );
return;
}
}
$updates = $this->get_translation_updates();
$current_locale = get_locale();
$translations = array();
$plugins = new \WP_CLI\Fetchers\Plugin();
foreach ( $args as $plugin ) {
if ( ! $plugins->get( $plugin ) ) {
WP_CLI::warning( "Plugin '{$plugin}' not found." );
continue;
}
$installed_translations = $this->get_installed_languages( $plugin );
$available_translations = $this->get_all_languages( $plugin );
foreach ( $available_translations as $translation ) {
$translation['plugin'] = $plugin;
$translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled';
if ( $current_locale === $translation['language'] ) {
$translation['status'] = 'active';
}
$filter_args = array(
'language' => $translation['language'],
'type' => 'plugin',
'slug' => $plugin,
);
$update = wp_list_filter( $updates, $filter_args );
$translation['update'] = $update ? 'available' : 'none';
// Support features like --status=active.
foreach ( array_keys( $translation ) as $field ) {
if ( isset( $assoc_args[ $field ] ) && $assoc_args[ $field ] !== $translation[ $field ] ) {
continue 2;
}
}
$translations[] = $translation;
}
}
$formatter = $this->get_formatter( $assoc_args );
$formatter->display_items( $translations );
}

That's where the following check (against an empty $installed_translations array returned by get_installed_languages()) results in the uninstalled value:

$translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled';

A solution is to account for the discrepancy between plugin slugs and text domains in the get_installed_languages() method.</issue_description>

Comments on the Issue (you are @copilot in this section)

@swissspidy Can you give an example of a plugin where this is happening?

How would WP-CLI know what the correct text domain would be for the plugin?

WordPress plugins are really meant to have matching slugs and text domains. If they don't match, this kind of situation is to be expected. I assume WordPress itself also doesn't recognize the translations as being installed in that case.</comment_new>
<comment_new>@swissspidy
> It does because it relies on the text domain, not the plugin slug.

For loading, yes, but for updates I don't think WP has any such logic. At least I can't find it.

Looking at the Text Domain header sounds like an interesting hardening step, but also a bit unusual because of that.</comment_new>

Original prompt

This section details on the original issue you should resolve

<issue_title>Incorrect language installation status when plugin's slug is different from its text domain</issue_title>
<issue_description>## Bug Report

Describe the current, buggy behavior

When listing available languages for a plugin using the wp language plugin list <plugin> command, the status is always uninstalled if the plugin's slug ≠ plugin's text domain.

This is due to the wp_get_installed_translations() method used in

protected function get_installed_languages( $slug = 'default' ) {
$available = wp_get_installed_translations( $this->obj_type );
$available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array();
$available[] = 'en_US';
return $available;
}

wp_get_installed_translations() returns an array keyed by the text domain. As a result, the subsequent check for available languages ($available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array();) results in an empty array if the plugin's slug is different from its text domain.

get_installed_languages() is used by the list command:

public function list_( $args, $assoc_args ) {
$all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false );
if ( ! $all && empty( $args ) ) {
WP_CLI::error( 'Please specify one or more plugins, or use --all.' );
}
if ( $all ) {
$args = array_map( '\WP_CLI\Utils\get_plugin_name', array_keys( $this->get_all_plugins() ) );
if ( empty( $args ) ) {
WP_CLI::success( 'No plugins installed.' );
return;
}
}
$updates = $this->get_translation_updates();
$current_locale = get_locale();
$translations = array();
$plugins = new \WP_CLI\Fetchers\Plugin();
foreach ( $args as $plugin ) {
if ( ! $plugins->get( $plugin ) ) {
WP_CLI::warning( "Plugin '{$plugin}' not found." );
continue;
}
$installed_translations = $this->get_installed_languages( $plugin );
$available_translations = $this->get_all_languages( $plugin );
foreach ( $available_translations as $translation ) {
$translation['plugin'] = $plugin;
$translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled';
if ( $current_locale === $translation['language'] ) {
$translation['status'] = 'active';
}
$filter_args = array(
'language' => $translation['language'],
'type' => 'plugin',
'slug' => $plugin,
);
$update = wp_list_filter( $updates, $filter_args );
$translation['update'] = $update ? 'available' : 'none';
// Support features like --status=active.
foreach ( array_keys( $translation ) as $field ) {
if ( isset( $assoc_args[ $field ] ) && $assoc_args[ $field ] !== $translation[ $field ] ) {
continue 2;
}
}
$translations[] = $translation;
}
}
$formatter = $this->get_formatter( $assoc_args );
$formatter->display_items( $translations );
}

That's where the following check (against an empty $installed_translations array returned by get_installed_languages()) results in the uninstalled value:

$translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled';

A solution is to account for the discrepancy between plugin slugs and text domains in the get_installed_languages() method.</issue_description>

Comments on the Issue (you are @copilot in this section)

@swissspidy Can you give an example of a plugin where this is happening?

How would WP-CLI know what the correct text domain would be for the plugin?

WordPress plugins are really meant to have matching slugs and text domains. If they don't match, this kind of situation is to be expected. I assume WordPress itself also doesn't recognize the translations as being installed in that case.</comment_new>
<comment_new>@swissspidy

It does because it relies on the text domain, not the plugin slug.

For loading, yes, but for updates I don't think WP has any such logic. At least I can't find it.

Looking at the Text Domain header sounds like an interesting hardening step, but also a bit unusual because of that.</comment_new>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits November 8, 2025 16:23
…n slug

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix incorrect language installation status for plugins Fix language status detection for plugins/themes with text domain different from slug Nov 8, 2025
Copilot AI requested a review from swissspidy November 8, 2025 16:33
Copilot finished work on behalf of swissspidy November 8, 2025 16:33
@swissspidy
Copy link
Member

@copilot you added a test for plugins, but can you add one for themes as well?

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copy link
Author

Copilot AI commented Nov 10, 2025

@copilot you added a test for plugins, but can you add one for themes as well?

Done! Added a test scenario for themes in commit a770faf. The test creates a theme with a different text domain and verifies that installed translations are correctly detected via the text domain rather than the slug.

Copilot finished work on behalf of swissspidy November 10, 2025 15:58
@swissspidy swissspidy marked this pull request as ready for review November 10, 2025 16:12
@swissspidy swissspidy requested a review from a team as a code owner November 10, 2025 16:12
@codecov
Copy link

codecov bot commented Nov 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect language installation status when plugin's slug is different from its text domain

2 participants