Skip to content

Always set the Language property on translation updates - #187

Open
victorcamnerin wants to merge 1 commit into
wp-cli:mainfrom
victorcamnerin:fix/undefined-language-property
Open

victorcamnerin wants to merge 1 commit into
wp-cli:mainfrom
victorcamnerin:fix/undefined-language-property

Conversation

@victorcamnerin

@victorcamnerin victorcamnerin commented Aug 31, 2026 •

Copy link
Copy Markdown

Summary

CommandWithTranslation::update() logs every pack it is about to install with
"Updating '{$update->Language}' translation for {$update->Name} {$update->Version}...",
but Language is only assigned inside an isset( $translation->english_name ) guard. When
that lookup misses, the property is never set, so PHP 8 raises
Warning: Undefined property: stdClass::$Language and the locale prints as an empty string.

The two sides can legitimately disagree:

  • get_translation_updates() filters the update check through plugins_update_check_locales,
    so the transient offers a pack for every locale installed on the site.
  • get_all_languages() then re-queries translations_api() pinned to the installed
    extension version.

When that response doesn't list the locale, wp_list_filter() returns an empty array,
reset() yields false, and (object) false has no english_name.

This is easy to hit on a multi-locale site with a mixed set of extensions — we see it a few
times a day on a production site running five locales, from a nightly
wp language plugin update --all. It is cosmetic (it happens before $upgrader->upgrade(),
and the packs still install), but it produces a steady stream of PHP warnings in cron output
and error trackers. The guard has been unchanged since v2.0.0.

Falling back to the raw locale code keeps the line meaningful: Updating 'de_DE' translation for Akismet 4.0... rather than Updating '' translation for Akismet 4.0....

Testing

Adds a Behat scenario that mocks the translations API to return {"translations":[]} after
the update check has already offered a de_DE pack, then asserts STDERR is empty. It fails
on main and passes with this change.

I could not run composer phpcs or composer behat locally — my environment has no network
access to codeload.github.com, so dev dependencies would not install. Only php -l was
verified here; please let CI be the judge on the rest.

Summary by CodeRabbit

  • Bug Fixes
    • Translation updates now proceed when the translation service does not list the installed translation.
    • Improved reliability when updating plugin translations after a plugin upgrade.

`wp language <core|plugin|theme> update` reads `$update->Language` when
logging each pack it is about to install, but only assigned it when the
translations API returned an `english_name` for that locale.

The two sides can disagree: the update check offers packs for every
locale installed on the site, while `get_all_languages()` re-queries the
translations API pinned to the *installed* extension version. When that
lookup returns nothing, `reset()` yields false, the property is never
set, and PHP 8 raises "Warning: Undefined property: stdClass::$Language"
before printing the locale as an empty string.

Fall back to the raw locale code so the line stays meaningful and the
command stays warning-free.
@victorcamnerin
victorcamnerin requested a review from a team as a code owner August 31, 2026 07:54
@github-actions

Copy link
Copy Markdown
Contributor

Hello! 👋

Thanks for opening this pull request! Please check out our contributing guidelines. We appreciate you taking the initiative to contribute to this project.

Contributing isn't limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.

Here are some useful Composer commands to get you started:

  • composer install: Install dependencies.
  • composer test: Run the full test suite.
  • composer phpcs: Check for code style violations.
  • composer phpcbf: Automatically fix code style violations.
  • composer phpunit: Run unit tests.
  • composer behat: Run behavior-driven tests.

To run a single Behat test, you can use the following command:

# Run all tests in a single file
composer behat features/some-feature.feature

# Run only a specific scenario (where 123 is the line number of the "Scenario:" title)
composer behat features/some-feature.feature:123

You can find a list of all available Behat steps in our handbook.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b31b5a0-0be7-41fa-8cc1-43602fc94bfe

📥 Commits

Reviewing files that changed from the base of the PR and between 1929dfe and 5b1b51f.

📒 Files selected for processing (2)
  • features/language-plugin.feature
  • src/WP_CLI/CommandWithTranslation.php

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The translation update path now falls back to the existing language value when the API does not provide english_name. A Gherkin scenario verifies that Akismet's de_DE translation still updates when the API returns an empty list.

Changes

Translation update fallback

Layer / File(s) Summary
Language fallback and regression coverage
src/WP_CLI/CommandWithTranslation.php, features/language-plugin.feature
The update object always receives english_name or language. The new scenario verifies the update output and empty STDERR when the translations API returns no languages.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 5b1b5

The update now shows the locale code instead of emitting an undefined-property warning when translation metadata is unavailable. No actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: brianhenryie, ernilambar, janw-me

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main code change: always assigning the Language property on translation updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 PHPStan (2.2.8)

Invalid configuration:
Unexpected item 'parameters › strictRules'.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added bug command:language Related to 'language' command command:language-plugin Related to 'language plugin' command scope:testing Related to testing labels Aug 31, 2026
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@swissspidy

Copy link
Copy Markdown
Member

This is easy to hit on a multi-locale site with a mixed set of extensions

Is it? I would appreciate some clear reproduction steps. Sounds like the translation API is returning some fields but not all? 🤔

@victorcamnerin

Copy link
Copy Markdown
Author

Turns out my description was off. I dug into the two sites that log this, and the packs that trigger it never come from wordpress.org in the first place.

Plugins that host their own language packs append entries to the update_plugins transient via site_transient_update_plugins / pre_set_site_transient_update_plugins. The TranslationsPress library does this (Yoast SEO Premium, Gravity Forms and its add-ons), and WC_Helper_Updater does the same for WooCommerce.com extensions. get_translation_updates() picks those entries up, but get_all_languages() then asks the wordpress.org translations API for that slug, and wordpress.org has never heard of it:

$ curl 'https://api.wordpress.org/translations/plugins/1.0/?slug=wordpress-seo-premium&version=27.5'
{"translations":[]}

So it's not a partial response, there's simply no entry for the locale. wp_list_filter() returns [] and Language never gets set. Gravity Forms also filters translations_api to fill that gap, so its packs print fine. Yoast SEO Premium filters it too, but returns the data keyed by slug instead of under translations, so the lookup still misses. Locale count doesn't matter, a single-locale site hits it just as easily, so the "multi-locale" angle in the description was a red herring.

Reproduction (no premium plugin needed, any WP install with WP_DEBUG on):

wp scaffold plugin my-private-plugin --skip-tests

wp-content/mu-plugins/private-language-packs.php:

<?php
add_filter( 'site_transient_update_plugins', function ( $value ) {
	if ( ! is_object( $value ) ) {
		$value = new stdClass();
	}
	$value->translations[] = array(
		'type'       => 'plugin',
		'slug'       => 'my-private-plugin',
		'language'   => 'de_DE',
		'version'    => '0.1.0',
		'updated'    => '2026-01-01 00:00:00',
		'package'    => 'https://example.invalid/my-private-plugin-de_DE.zip',
		'autoupdate' => true,
	);
	return $value;
} );
$ wp language plugin update my-private-plugin
Warning: Undefined property: stdClass::$Language in .../language-command/src/WP_CLI/CommandWithTranslation.php on line 134
Updating '' translation for My Private Plugin 0.1.0...

Without WP_DEBUG the warning is swallowed and you're left with the empty quotes.

You could argue this is on the plugins to return the right shape from translations_api, so whether WP-CLI should guard against it too is your call. Happy to adjust the PR either way, or close it if you'd rather leave the current behaviour.

@swissspidy

Copy link
Copy Markdown
Member

Thanks for digging into this, that's a much clearer picture.

Those plugins are wrong: if you inject packs into update_plugins, you should also answer translations_api for your slug, and in the same shape wordpress.org uses ({"translations": [...]}). Gravity Forms and e.g. Traduttore Registry do exactly that and are unaffected. Yoast's slug-keyed response is a bug on their side and worth reporting to them.

That said, I'm fine with landing the fallback. Reading $update->Language when we only set it conditionally is our bug, regardless of what the API returned, and core's own upgrader skin only shows the locale code anyway. The packs install fine either way, so degrading to de_DE is the right behaviour.

One ask before merging: please update the PR description to match the actual cause (a pack in the transient whose slug the translations API doesn't know), since the current text still tells the multi-locale / version-pinning story.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues were identified.

Review effort: Lite
Findings: None

What changed in this PR

Fixes translation update logging by falling back to the raw locale when translation metadata is unavailable.

Changes:

  • Always assigns the translation Language property.
  • Adds Behat coverage for missing translation metadata.
File Description
src/​WP_CLI/​CommandWithTranslation.php Adds locale fallback handling.
features/​language-plugin.feature Tests updates when the API returns no translations.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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

Labels

bug command:language Related to 'language' command command:language-plugin Related to 'language plugin' command scope:testing Related to testing state:unconfirmed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants