Skip to content

fix(i18n): defer translations until init to silence WP 6.7 textdomain notice#1166

Merged
superdav42 merged 1 commit intomainfrom
fix/textdomain-load-too-early
May 9, 2026
Merged

fix(i18n): defer translations until init to silence WP 6.7 textdomain notice#1166
superdav42 merged 1 commit intomainfrom
fix/textdomain-load-too-early

Conversation

@superdav42
Copy link
Copy Markdown
Collaborator

@superdav42 superdav42 commented May 9, 2026

Summary

Fix the WordPress 6.7+ _load_textdomain_just_in_time doing_it_wrong notice for the ultimate-multisite text domain by deferring two paths that translated strings before the init action fires.

Symptom

PHP Notice: Function _load_textdomain_just_in_time was called incorrectly.
Translation loading for the ultimate-multisite domain was triggered too early.
This is usually an indicator for some code in the plugin or theme running too early.
Translations should be loaded at the init action or later.
(This message was added in version 6.7.0.)

Root cause (reproduced)

Instrumented WordPress's gettext filter to log any __() call made before did_action('init'). On origin/main two distinct call sites trigger early translations on every request:

  1. WPEngine_Integration::__construct() at inc/integrations/providers/wpengine/class-wpengine-integration.php:35,37. Integration_Registry::fire_registration_hooks() is hooked to plugins_loaded priority 9, which instantiates 17 provider classes — only WPEngine sets its description with inline __() instead of overriding get_description() (the lazy pattern used by the other 16 providers).

  2. WP_CLI trait enable_wp_cli() at inc/apis/trait-wp-cli.php:79,95,100,130-150,178,193,212,221,237. Every Manager's init() synchronously calls enable_wp_cli() during plugin bootstrap, which calls __() while registering WP-CLI command shortdescs. WP-CLI's own bootstrap loads WordPress before init fires, so the textdomain just-in-time loader trips the WP 6.7 notice on every WP-CLI run.

Trace excerpt (reproduced locally on WP 7.x dev install):

EARLY TRANSLATION: "WP Engine drives your business forward faster..."
  did_plugins_loaded=1 did_init=0 current_action=gettext
  # 4 inc/integrations/providers/wpengine/class-wpengine-integration.php:35 __
  # 5 inc/integrations/class-integration-registry.php:133 WPEngine_Integration->__construct
  # 6 inc/integrations/class-integration-registry.php:100 register_core_integrations
  # 7 wp-includes/class-wp-hook.php:341 Integration_Registry->fire_registration_hooks (plugins_loaded:9)

EARLY TRANSLATION: "Manages %ss."
  # 4 inc/apis/trait-wp-cli.php:95 __
  # 5 inc/managers/class-event-manager.php:76 Event_Manager->enable_wp_cli
  # 6 inc/traits/trait-singleton.php:36 Event_Manager->init
  # 7 inc/class-wp-ultimo.php:921 Event_Manager::get_instance
  # 8 inc/class-wp-ultimo.php:245 WP_Ultimo->load_managers

Changes

inc/integrations/providers/wpengine/class-wpengine-integration.php

  • Removed the __() calls from the constructor.
  • Added a get_description() override that runs the translations on demand. This matches the pattern already used by Closte, Cloudways, RunCloud, cPanel, ServerPilot, GridPane, Cloudflare, Hestia, Enhance, Plesk, Rocket, WPMUDEV, BunnyNet, LaravelForge, Amazon SES, and CyberPanel — WPEngine was the odd one out.

inc/apis/trait-wp-cli.php

  • enable_wp_cli() keeps its public signature so every existing Manager continues to call it from init() unchanged.
  • The body now schedules a single add_action('init', [$this, 'register_wp_cli_commands'], 0).
  • A new register_wp_cli_commands() method holds the original registration logic (the __() calls move into it). It runs on init priority 0, before WP-CLI dispatches commands, so wp wu … continues to work without any other code change.

Verification

  • Repro probe (gettext filter mu-plugin, logs every translation before init):
    • Before: 6+ early-translation events per request — WPEngine description (×2 on every page load) and 6 WP-CLI shortdescs per Manager × N managers.
    • After: 0 events on the same probe.
  • wp wu lists all sub-commands (customer, membership, payment, product, registered_domain, site, webhook).
  • wp wu customer list --format=count runs cleanly.
  • vendor/bin/phpcs inc/integrations/providers/wpengine/class-wpengine-integration.php inc/apis/trait-wp-cli.php → clean.
  • vendor/bin/phpstan analyse inc/integrations/providers/wpengine/class-wpengine-integration.php inc/apis/trait-wp-cli.php → no errors.
  • vendor/bin/phpunit --filter 'Integration_Registry_Test|Provider_Integration_Test|Event_Manager_Test' → 127 tests, 1 unrelated pre-existing failure (amazon-ses missing domain-mapping capability, also fails on origin/main).

Notes

  • No public API changed. enable_wp_cli() signature is preserved; the only behavioural difference is that command registration now happens on init priority 0 rather than synchronously during the Manager's init() method.
  • register_wp_cli_commands() is public because the trait may be used by classes that compose it differently; the method is referenced only via the add_action callback.

aidevops.sh v3.15.12 plugin for OpenCode v1.14.41 with claude-sonnet-4-6 spent 13h 24m and 40 tokens on this as a headless worker.

Summary by CodeRabbit

  • Refactor
    • Optimized WP-CLI command registration for improved performance.
    • Enhanced WPEngine integration initialization efficiency with deferred description loading.

Review Change Stack

… notice

WordPress 6.7 added a _doing_it_wrong notice when a plugin triggers
the just-in-time textdomain loader before the 'init' action fires.
Two paths in Ultimate Multisite were calling __() too early:

1. WPEngine_Integration::__construct() built the description with __()
   inline and called set_description(). Integration_Registry instantiates
   every provider on plugins_loaded priority 9, before init.
   Fix: follow the lazy pattern already used by the other 16 integration
   providers — override get_description() so the translation runs only
   when the wizard or admin UI requests it (always after init).

2. The WP_CLI trait's enable_wp_cli() registered commands synchronously
   from each Manager's init() (called during plugin bootstrap, also
   before the init action). Each registration passed translated
   shortdescs through __() and sprintf(__(...)).
   Fix: enable_wp_cli() now schedules a single add_action('init', ...)
   at priority 0; the heavy registration moves into a new
   register_wp_cli_commands() method that runs on the init hook. WP-CLI
   still sees the commands because its own command dispatch happens after
   WordPress finishes bootstrapping.

Verified by instrumenting WP's gettext filter to log any translation
attempted before did_action('init'): 6+ events on origin/main, 0 events
after this change. wp wu sub-commands still register and run normally.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces two independent refactorings: WP-CLI command registration is deferred to WordPress's init action at priority 0 via a new public method to align with translation timing, and WPEngine integration description is moved from eager constructor initialization to lazy loading through a new get_description() method.

Changes

Deferred WP-CLI Command Registration

Layer / File(s) Summary
New Registration Method
inc/apis/trait-wp-cli.php
New public method register_wp_cli_commands() handles WP-CLI setup with early exit when WP_CLI is undefined, followed by root command/namespace and entity subcommand registration.
Documentation & Behavior
inc/apis/trait-wp-cli.php
Updated enable_wp_cli() docstring and comments explain deferred registration to WordPress init action at priority 0 for translation timing alignment.
Integration Hook Setup
inc/apis/trait-wp-cli.php
enable_wp_cli() now schedules command registration via add_action('init', 'register_wp_cli_commands', 0) instead of direct execution.

Lazy Description Loading in WPEngine Integration

Layer / File(s) Summary
Lazy Description Getter
inc/integrations/providers/wpengine/class-wpengine-integration.php
New public method get_description(): string lazily constructs and returns translated integration description via __() including wildcard-domain recommendation.
Constructor Refactoring
inc/integrations/providers/wpengine/class-wpengine-integration.php
Constructor no longer sets description eagerly; now only initializes logo, tutorial link, constants, and supported capabilities. Translation-dependent description deferred to lazy get_description() calls.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Deferred and lazy, two steps so wise,
Registration waits while descriptions arise,
No eager translations clog up the way,
Just hooks and getters, called when we play!

🚥 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 accurately describes the main change: deferring translations until the init action to resolve a WordPress 6.7 textdomain notice, which is the core purpose of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/textdomain-load-too-early

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 and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/integrations/providers/wpengine/class-wpengine-integration.php`:
- Around line 49-59: The public method get_description in
class-wpengine-integration.php should not declare a PHP return type; remove the
": string" return type from the get_description method signature so the method
matches the coding guideline for inc/integrations/ and remains extensible by
addons that may override it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b63ad9ab-9d74-4ba0-9f48-c6886941ed72

📥 Commits

Reviewing files that changed from the base of the PR and between b3c8f2b and ee803c6.

📒 Files selected for processing (2)
  • inc/apis/trait-wp-cli.php
  • inc/integrations/providers/wpengine/class-wpengine-integration.php

Comment on lines +49 to +59
/**
* {@inheritdoc}
*/
public function get_description(): string {

$description = __('WP Engine drives your business forward faster with the first and only WordPress Digital Experience Platform. We offer the best WordPress hosting and developer experience on a proven, reliable architecture that delivers unparalleled speed, scalability, and security for your sites.', 'ultimate-multisite');

$description .= '<br><br><b>' . __('We recommend to enter in contact with WP Engine support to ask for a Wildcard domain if you are using a subdomain install.', 'ultimate-multisite') . '</b>';

return $description;
}
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove PHP return type declaration from public method.

The : string return type on line 52 violates the coding guideline for this directory. As per coding guidelines, public methods in inc/integrations/ must not use PHP return type declarations because external addons may extend these classes, and PHP will fatal if child classes don't match the parent's return type signature.

🔧 Proposed fix
 	/**
 	 * {`@inheritdoc`}
+	 *
+	 * `@return` string
 	 */
-	public function get_description(): string {
+	public function get_description() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* {@inheritdoc}
*/
public function get_description(): string {
$description = __('WP Engine drives your business forward faster with the first and only WordPress Digital Experience Platform. We offer the best WordPress hosting and developer experience on a proven, reliable architecture that delivers unparalleled speed, scalability, and security for your sites.', 'ultimate-multisite');
$description .= '<br><br><b>' . __('We recommend to enter in contact with WP Engine support to ask for a Wildcard domain if you are using a subdomain install.', 'ultimate-multisite') . '</b>';
return $description;
}
/**
* {`@inheritdoc`}
*
* `@return` string
*/
public function get_description() {
$description = __('WP Engine drives your business forward faster with the first and only WordPress Digital Experience Platform. We offer the best WordPress hosting and developer experience on a proven, reliable architecture that delivers unparalleled speed, scalability, and security for your sites.', 'ultimate-multisite');
$description .= '<br><br><b>' . __('We recommend to enter in contact with WP Engine support to ask for a Wildcard domain if you are using a subdomain install.', 'ultimate-multisite') . '</b>';
return $description;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/integrations/providers/wpengine/class-wpengine-integration.php` around
lines 49 - 59, The public method get_description in
class-wpengine-integration.php should not declare a PHP return type; remove the
": string" return type from the get_description method signature so the method
matches the coding guideline for inc/integrations/ and remains extensible by
addons that may override it.

@superdav42 superdav42 merged commit 685cf60 into main May 9, 2026
11 checks passed
@superdav42
Copy link
Copy Markdown
Collaborator Author

Summary

Fix the WordPress 6.7+ _load_textdomain_just_in_time doing_it_wrong notice for the ultimate-multisite text domain by deferring two paths that translated strings before the init action fires.

Symptom

PHP Notice: Function _load_textdomain_just_in_time was called incorrectly.
Translation loading for the ultimate-multisite domain was triggered too early.
This is usually an indicator for some code in the plugin or theme running too early.
Translations should be loaded at the init action or later.
(This message was added in version 6.7.0.)

Root cause (reproduced)

Instrumented WordPress's gettext filter to log any __() call made before did_action('init'). On origin/main two distinct call sites trigger early translations on every request:

  1. WPEngine_Integration::__construct() at inc/integrations/providers/wpengine/class-wpengine-integration.php:35,37. Integration_Registry::fire_registration_hooks() is hooked to plugins_loaded priority 9, which instantiates 17 provider classes — only WPEngine sets its description with inline __() instead of overriding get_description() (the lazy pattern used by the other 16 providers).
  2. WP_CLI trait enable_wp_cli() at inc/apis/trait-wp-cli.php:79,95,100,130-150,178,193,212,221,237. Every Manager's init() synchronously calls enable_wp_cli() during plugin bootstrap, which calls __() while registering WP-CLI command shortdescs. WP-CLI's own bootstrap loads WordPress before init fires, so the textdomain just-in-time loader trips the WP 6.7 notice on every WP-CLI run.
    Trace excerpt (reproduced locally on WP 7.x dev install):
EARLY TRANSLATION: "WP Engine drives your business forward faster..."
  did_plugins_loaded=1 did_init=0 current_action=gettext
  # 4 inc/integrations/providers/wpengine/class-wpengine-integration.php:35 __
  # 5 inc/integrations/class-integration-registry.php:133 WPEngine_Integration->__construct
  # 6 inc/integrations/class-integration-registry.php:100 register_core_integrations
  # 7 wp-includes/class-wp-hook.php:341 Integration_Registry->fire_registration_hooks (plugins_loaded:9)
EARLY TRANSLATION: "Manages %ss."
  # 4 inc/apis/trait-wp-cli.php:95 __
  # 5 inc/managers/class-event-manager.php:76 Event_Manager->enable_wp_cli
  # 6 inc/traits/trait-singleton.php:36 Event_Manager->init
  # 7 inc/class-wp-ultimo.php:921 Event_Manager::get_instance
  # 8 inc/class-wp-ultimo.php:245 WP_Ultimo->load_managers

Changes

inc/integrations/providers/wpengine/class-wpengine-integration.php

  • Removed the __() calls from the constructor.
  • Added a get_description() override that runs the translations on demand. This matches the pattern already used by Closte, Cloudways, RunCloud, cPanel, ServerPilot, GridPane, Cloudflare, Hestia, Enhance, Plesk, Rocket, WPMUDEV, BunnyNet, LaravelForge, Amazon SES, and CyberPanel — WPEngine was the odd one out.

inc/apis/trait-wp-cli.php

  • enable_wp_cli() keeps its public signature so every existing Manager continues to call it from init() unchanged.
  • The body now schedules a single add_action('init', [$this, 'register_wp_cli_commands'], 0).
  • A new register_wp_cli_commands() method holds the original registration logic (the __() calls move into it). It runs on init priority 0, before WP-CLI dispatches commands, so wp wu … continues to work without any other code change.

Verification

  • Repro probe (gettext filter mu-plugin, logs every translation before init):
    • Before: 6+ early-translation events per request — WPEngine description (×2 on every page load) and 6 WP-CLI shortdescs per Manager × N managers.
    • After: 0 events on the same probe.
  • wp wu lists all sub-commands (customer, membership, payment, product, registered_domain, site, webhook).
  • wp wu customer list --format=count runs cleanly.
  • vendor/bin/phpcs inc/integrations/providers/wpengine/class-wpengine-integration.php inc/apis/trait-wp-cli.php → clean.
  • vendor/bin/phpstan analyse inc/integrations/providers/wpengine/class-wpengine-integration.php inc/apis/trait-wp-cli.php → no errors.
  • vendor/bin/phpunit --filter 'Integration_Registry_Test|Provider_Integration_Test|Event_Manager_Test' → 127 tests, 1 unrelated pre-existing failure (amazon-ses missing domain-mapping capability, also fails on origin/main).

Notes

  • No public API changed. enable_wp_cli() signature is preserved; the only behavioural difference is that command registration now happens on init priority 0 rather than synchronously during the Manager's init() method.
  • register_wp_cli_commands() is public because the trait may be used by classes that compose it differently; the method is referenced only via the add_action callback.

aidevops.sh v3.15.12 plugin for OpenCode v1.14.41 with claude-sonnet-4-6 spent 13h 24m and 40 tokens on this as a headless worker.


Merged via PR #1166 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Performance Test Results

Performance test results for 2d78204 are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 41 37.78 MB 822.00 ms (-100.50 ms / -12% ) 192.00 ms (+19.50 ms / +10% ) 1120.00 ms (+23.00 ms / +2% ) 2078.00 ms 1983.70 ms 87.45 ms
1 56 49.12 MB 947.00 ms 147.00 ms 1092.50 ms 2092.00 ms 2015.00 ms 79.45 ms

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.

1 participant