Skip to content

feat: update-translations applies the language packs offered (#352) - #358

Merged
ifahimreza merged 7 commits into
mainfrom
feat/352-update-translations
Oct 12, 2026
Merged

ifahimreza merged 7 commits into
mainfrom
feat/352-update-translations

Conversation

@ifahimreza

@ifahimreza ifahimreza commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #352

Stacked on #357 (base feat/351-update-core). Merge #353 → #354 → #355 → #356 → #357 → this, with merge commits.

What

A new tool, saddle/update-translations. It applies every translation update WordPress offers, for WordPress, plugins and themes, through core's own language pack updater in the background runner, after a confirmed preview.

Why

The last of #347's tools. list-updates already counts the translations waiting; until now an app could only report them.

How

  • Admin tier, destructive, open world, gated. The only input is confirm_token. Permission cap activate_plugins; the handler checks update_languages after any blocked_by() reason (context WP_CONTENT_DIR, the one core's own updater uses for language packs).
  • Preview: each pack's id (type:slug:language), its name as core's Language_Pack_Upgrader::get_name_for_update() gives it plus the language (e.g. "Akismet (de_DE)"), and when it was updated. The token binds that exact list.
  • No item list and no 10-item cap, unlike update-plugin. WordPress applies every offered pack on its own in each background run, and sites often have dozens; making an app page through them would be busywork.
  • Run: Saddle_Offer_Runner gets the offers from wp_get_translation_updates(). A one-shot auto_update_translation filter says yes to the confirmed ids only. Each pack goes through WP_Automatic_Updater::update( 'translation', … ), which is core's Language_Pack_Upgrader; it only replaces that language's files. Log lines read "Updated the Akismet (de_DE) translation."
  • Copy: an undo reason (no copy of the old files is kept), the list-updates description, the readme Updates and Undo bullets, a 1.5.2 changelog line, ROADMAP, and the .pot. No refusal-playbook line: the refusals here name their own fix, and the context is paid on every session.

Testing

  • phpunit on PHP 8.2: 1508 tests, 8602 assertions, 1 skipped (the existing 11-plugin test).
  • phpcs on every changed PHP file: clean. The six execution functions: no calls.
  • tests/core-updates-test.php has 5 new tests:
    • the tier test
    • a preview listing the pack and changing nothing
    • a confirm that queues with zero upgrader calls, and single-use tokens
    • a token failing when the list changed
    • nothing offered, no update_languages, and file changes forbidden
    • a real language pack applied by the runner through core from a local zip, landing in WP_LANG_DIR/plugins/
  • Not run on a live install: the Playground sites were English with no packs offered. The unit test drives core's real Language_Pack_Upgrader end to end.

The whole stack, for review

PR Tool +/− lines (all) without the .pot
#353 docs: the hard line +48 / −15 same
#354 search-plugins +808 / −54 +705 / −1
#355 install-plugin, Saddle_Directory, Saddle_Install_Runner +1195 / −270 +1027 / −218
#356 install-theme +727 / −258 +603 / −199
#357 update-core, Saddle_Offer_Runner +1131 / −238 +938 / −139
#358 update-translations +371 / −51 +286 / −15

Tests and recorded fixtures are a third to a half of each code PR (+366, +345, +259, +434, +136).

Every class stays under ~300 lines: Saddle_Update_Runner 306, Saddle_Offer_Runner 230, Saddle_Install_Runner 253, Saddle_Directory 242, Saddle_Install_Abilities 289, Saddle_Core_Update_Abilities 280.

🤖 Generated with Claude Code

ifahimreza and others added 7 commits October 12, 2026 04:37
Rewrite the hard line before any code: Saddle installs a plugin or
theme only from the WordPress.org directory, by slug, through
WordPress's own installer, and the new install arrives inactive. Core
updates are minor and security releases, plus a major one only behind
WordPress's own switch for all new versions. Translations go through
core's language pack upgrader. Nothing deletes a plugin or theme. Adds
the two "What NOT to do" bullets and the ROADMAP entry.

Refs #347

Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, read-only tool. It asks the WordPress.org directory
through core's own plugins_api(), the same request Plugins > Add New
makes, by search words or one exact slug, at most 10 results. Each row
has the slug, name, author, version, active installs, rating, last
update, tested up to, the WordPress and PHP it needs, and whether it is
installed and active here, by core's install_plugin_install_status().
The directory answer is cached for an hour; the installed state never
is. Tests answer the directory from responses recorded from the live
API, which pins the field names.

Refs #348

Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool. One slug per call. The preview asks the
directory fresh and shows the version, installs, rating, last update,
tested-up-to against this WordPress and the PHP it needs, with warnings
for untested, two years without an update, or plugins it needs. The
token binds the version. Refused before any token: a site that forbids
file changes, an account without install_plugins, an unknown or closed
slug, an installed plugin or a leftover folder, PHP or WordPress too
old, and a package outside downloads.wordpress.org.

A confirm queues the install on the existing runner; the request never
calls the installer (pinned, and the update-plugin test now counts
upgrader calls too). Saddle_Install_Runner asks the directory again,
installs only the confirmed version through Plugin_Upgrader::install(),
and the plugin arrives inactive. Saddle_Directory holds the lookups and
refusals both sides need. The installed check reads core's rule
without install_plugin_install_status(), which forces an update check.

Refs #349

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ated

Same shape as install-plugin: one slug, a fresh directory answer
through themes_api(), a preview with the version, installs, rating,
last update and the WordPress and PHP it needs, a token bound to the
version, a background run through Theme_Upgrader::install(). The theme
is never activated. The permission capability is switch_themes and the
handler checks install_themes after naming any blocked_by() reason.

Core's theme installer fetches a missing parent theme on its own. A
child theme whose parent is not installed is refused before a token,
the run checks again (the folder too, since WP_Theme can answer from
the object cache after a delete), and during the run the directory
answers only the confirmed slug.

Saddle_Directory now only talks to the API. The installed check and
the refusals moved to Saddle_Install_Runner, where the run and the
abilities both reach them, as blocked_by() sits on the update runner.

Refs #350

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found end to end on Playground: install-theme over MCP failed with
"Call to undefined function themes_api()". The update runner's loader
keys the admin theme.php on wp_get_themes(), which wp-includes always
defines, so a REST request never loaded it. The test suite loads the
admin files up front, which hid it.

Refs #350

Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool. It reads the "autoupdate" entries of the
update_core transient, the releases WordPress would apply in the
background (get_core_updates() leaves them out), and picks the newest
minor release, else a major one only when Core_Upgrader says WordPress
would apply it itself. The preview shows the from and to version, minor
or major, the PHP and MySQL needed, and for a major the active plugins
not tested with it and what cannot be undone. The token binds the
release. Refused before a token: blocked_by() reasons, no update_core,
a development version, a recorded critical failure, PHP or MySQL too
old, a major WordPress would not apply, nothing offered.

Found while testing: since WordPress 5.6 a new install defaults
auto_update_core_major to "enabled", so on most sites WordPress's own
switch already allows majors. The copy now says "when WordPress is set
to update itself to all new versions" instead of "when the owner turned
it on", and a test pins core's default.

The runner was at 461 lines, so the offer path (offers, the one-shot
auto_update filter, the apply loop, result recording) moved to
Saddle_Offer_Runner beside Saddle_Install_Runner; Saddle_Update_Runner
keeps scheduling, guards, the lock and the run store (304 lines). The
offer path also closes the output buffer core's skin opens on failure.

Refs #351

Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool with no input but the token. The preview
lists every translation WordPress offers (what it translates, the
language, when it was updated), named the way core's
Language_Pack_Upgrader names them, and the token binds that exact list.
No 10-item cap: WordPress applies every offered pack on its own in each
background run, and sites often have dozens. Refused when nothing is
offered, without update_languages, or for any blocked_by() reason (the
context is WP_CONTENT_DIR, as core checks language packs).

The offer runner applies each pack through
WP_Automatic_Updater::update('translation'), core's language pack
upgrader, with the one-shot filter saying yes to the confirmed ids only.
A test applies a real pack from a local zip and finds it in
WP_LANG_DIR/plugins.

Refs #352

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ifahimreza
ifahimreza changed the base branch from feat/351-update-core to main October 12, 2026 10:03
@ifahimreza
ifahimreza merged commit 4b1db33 into main Oct 12, 2026
8 checks passed
@ifahimreza
ifahimreza deleted the feat/352-update-translations branch October 12, 2026 10:06
@ifahimreza ifahimreza mentioned this pull request Oct 12, 2026
12 tasks done
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.

update-translations: apply translation updates through core's updater

1 participant