Merge branch 'code/MajorAir/wp-textdomain-wiki'

This commit is contained in:
Marcus Summers 2026-06-21 11:44:52 -04:00
commit a45ef55862
2 changed files with 195 additions and 1 deletions

View file

@ -0,0 +1,193 @@
---
title: "WordPress 6.7 _load_textdomain_just_in_time Notice (Theme/Plugin Loads Translations Too Early)"
domain: troubleshooting
category: troubleshooting
tags:
- wordpress
- wordpress-6.7
- php
- i18n
- textdomain
- theme
- mu-plugin
- deprecation
- troubleshooting
status: published
created: 2026-06-21
updated: 2026-06-21
---
# WordPress 6.7 `_load_textdomain_just_in_time` Notice
> **TL;DR** — WordPress 6.7 added a `doing_it_wrong` notice that fires when a translation function (`__()`, `_e()`, `esc_html__()`, …) is called for a text domain **before the `init` action**. It's almost always a theme or plugin registering nav menus / sidebars / labels on `after_setup_theme` (which runs before `init`). The notice is **debug-only and harmless** — translations still load via the just-in-time fallback. If the offending code is in your own (or an updatable) theme/plugin, fix it at the source by deferring to `init`. If it's a **non-updating or third-party** theme you don't want to hand-edit, suppress *only this one notice* with a `doing_it_wrong_trigger_error` filter in a tiny mu-plugin.
---
## Symptom
With `WP_DEBUG` on (or in Query Monitor's PHP panel), you see:
```
Function _load_textdomain_just_in_time was called incorrectly.
Translation loading for the <domain> 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.)
_load_textdomain_just_in_time() wp-includes/l10n.php
get_translations_for_domain() wp-includes/l10n.php
translate() wp-includes/l10n.php
__() wp-includes/l10n.php
WordPress Core
```
The key fields are **the domain name** (e.g. `marstheme`, `woocommerce`, `astra`) and the fact that the stack bottoms out in **WordPress Core** via `__()` — that tells you *some* extension called a translation function, not that core is broken.
## Why it happens (the WP 6.7 change)
Before 6.7, WordPress silently "just-in-time" loaded a text domain the first time you translated a string in it. 6.7 kept the JIT loading but started **warning** when it's triggered before `init`, because:
- Translations loaded before `init` can't be filtered/overridden by other plugins that hook `init`.
- It signals the extension is doing setup work earlier than the WordPress lifecycle intends.
The usual culprit is code on **`after_setup_theme`** (which fires *before* `init`) that translates a label inline, e.g.:
```php
function mytheme_setup() {
register_nav_menus( array(
'primary' => __( 'Primary Menu', 'mytheme' ), // <-- translate call before init
) );
}
add_action( 'after_setup_theme', 'mytheme_setup' );
```
> **Important:** explicitly calling `load_theme_textdomain()` / `load_plugin_textdomain()` early does **not** fix the notice, and as of WP 4.6+ themes on wordpress.org don't even need to call it. The notice is about the *translate call*, not about whether the domain was loaded. Moving only the `load_*_textdomain()` call around is a common dead-end (see the gotcha below).
## Diagnostic chain
### 1. Identify the domain and what owns it
The notice names the domain. Find which theme/plugin uses it:
```bash
WPROOT=/var/www/html
grep -rlw '<domain>' "$WPROOT/wp-content/themes" "$WPROOT/wp-content/plugins" 2>/dev/null
# Which extension has the most references (i.e. owns the domain)?
grep -rl '<domain>' "$WPROOT/wp-content/" 2>/dev/null \
| sed -E "s#$WPROOT/wp-content/(themes|plugins|mu-plugins)/([^/]+)/.*#\1/\2#" \
| sort | uniq -c | sort -rn | head
```
> **Watch for renamed/forked themes.** The domain often does **not** match the theme's folder name. A theme bought as "Mars" and re-slugged to `kappa` keeps `marstheme` as its text domain in all 40+ template files. So `wp theme list` shows `kappa` active while the notice says `marstheme` — they're the same thing.
### 2. Confirm it's active and whether it can be updated
```bash
sudo -u www-data wp --path=$WPROOT theme list --fields=name,status,version,update
sudo -u www-data wp --path=$WPROOT plugin list --fields=name,status,version,update
```
- `update available`**update it first** (newest releases of most themes/plugins fixed this in late 2024/2025). That's the proper fix; the rest of this article is for when you can't.
- `update none` on a **renamed/custom fork** → no upstream exists, so updating is impossible. Go to the suppression fix.
### 3. Pin down the early call (optional)
```bash
grep -rn "__(\s*['\"].*['\"]\s*,\s*['\"]<domain>['\"]" \
"$WPROOT/wp-content/themes/<theme>" | head
```
Look for translate calls inside functions hooked to `after_setup_theme`, `setup_theme`, `plugins_loaded`, or run at file scope in `functions.php`.
## The fix
### Option A — fix it at the source (own / updatable code)
Defer the translation. Either register the raw string and translate at render time, or move the registration to `init`:
```php
// Before: translated on after_setup_theme (too early)
add_action( 'after_setup_theme', function () {
register_nav_menus( array( 'primary' => __( 'Primary Menu', 'mytheme' ) ) );
} );
// After: register the menu location on init, where translation is allowed
add_action( 'init', function () {
register_nav_menus( array( 'primary' => __( 'Primary Menu', 'mytheme' ) ) );
} );
```
Don't do this by editing a theme/plugin that receives updates — your change is wiped on the next update. Use Option B for those.
### Option B — suppress just this notice (third-party / non-updating code)
When the early call lives in a theme you don't control and can't update (a renamed commercial fork, an abandoned plugin), the clean, update-safe move is to silence **only** the `_load_textdomain_just_in_time` notice — not all `doing_it_wrong` output — via a must-use plugin.
Create `wp-content/mu-plugins/fix-textdomain.php`:
```php
<?php
/**
* Suppress the WP 6.7 "_load_textdomain_just_in_time was called incorrectly"
* notice for a theme/plugin that translates before init.
*
* Scope is intentionally narrow: only this one function is silenced, so other
* doing_it_wrong notices still surface. Translations still load via the JIT
* fallback, so nothing visible changes for visitors.
*/
add_filter( 'doing_it_wrong_trigger_error', function ( $trigger, $function_name ) {
return '_load_textdomain_just_in_time' === $function_name ? false : $trigger;
}, 10, 2 );
```
`mu-plugins/` loads automatically (no activation, can't be deactivated from the admin), and runs early enough to register the filter before the notice fires.
#### Verify
```bash
WPROOT=/var/www/html
# 1. Syntax-check the mu-plugin
php -l "$WPROOT/wp-content/mu-plugins/fix-textdomain.php"
# -> No syntax errors detected
# 2. Confirm WP still boots and the filter is registered
sudo -u www-data wp --path=$WPROOT eval \
'echo has_filter("doing_it_wrong_trigger_error") ? "filter set\n" : "MISSING\n";'
# 3. Clear the debug log, trigger an early translate, confirm 0 new notices
DBG="$WPROOT/wp-content/debug.log"
[ -f "$DBG" ] && : > "$DBG"
sudo -u www-data wp --path=$WPROOT eval '__("Primary Menu","<domain>");' >/dev/null 2>&1
grep -c "<domain>" "$DBG" 2>/dev/null || echo 0
# -> 0
```
## Gotchas
### The "load the textdomain earlier/later" dead-end
A very common (wrong) first attempt is an mu-plugin that just calls `load_theme_textdomain()` on `plugins_loaded` or `after_setup_theme`:
```php
// DOES NOT FIX THE NOTICE
add_action( 'plugins_loaded', function () {
load_theme_textdomain( 'mytheme', get_template_directory() . '/languages' );
}, 0 );
```
`plugins_loaded` still runs **before `init`**, and — more importantly — the notice is triggered by the theme's own early `__()` call, not by whether you've loaded the domain. This code is dead weight. If you find one in place, replace it with the Option B filter rather than tweaking its hook/priority.
### Don't blanket-suppress all deprecations
Resist `error_reporting(E_ALL & ~E_DEPRECATED)` or returning `false` from `doing_it_wrong_trigger_error` unconditionally — that also hides genuinely useful warnings (a plugin breaking on a future PHP/WP bump). Scope the filter to the one `function_name`.
### Renamed theme ⇒ domain ≠ folder
Re-stating because it costs the most time: the domain in the notice can be the theme's *original* slug, not its current folder. Always `grep` for the domain to find the real owner before concluding "I don't even have that theme installed."
## See also
- [Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages](php-84-vendor-implicit-nullable-patch.md) — the other "harmless deprecation that floods logs" pattern on the WordPress fleet
- [WordPress developer note: i18n improvements in 6.7](https://make.wordpress.org/core/2024/10/21/i18n-improvements-in-6-7/) — the canonical reference for this change

View file

@ -1,6 +1,6 @@
---
created: 2026-04-02T16:03
updated: 2026-06-19T10:05
updated: 2026-06-21T11:46
---
* [Home](index.md)
* [Linux & Sysadmin](01-linux/index.md)
@ -118,6 +118,7 @@ updated: 2026-06-19T10:05
* [Claude Desktop MCP Server Started via wsl.exe Sees Empty Environment (WSLENV)](05-troubleshooting/wsl-env-claude-desktop-mcp.md)
* [Claude Desktop MCP Mass-Disconnect After Blocking SSH Reboot](05-troubleshooting/claude-desktop-mcp-mass-disconnect-blocking-reboot.md)
* [Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages](05-troubleshooting/php-84-vendor-implicit-nullable-patch.md)
* [WordPress 6.7 `_load_textdomain_just_in_time` Notice (Translations Loaded Too Early)](05-troubleshooting/wordpress-67-textdomain-just-in-time-notice.md)
* [Ollama Drops Off Tailscale When Mac Sleeps](05-troubleshooting/ollama-macos-sleep-tailscale-disconnect.md)
* [Ollama: `ollama run` with Piped Stdin Bypasses Chat Template + SYSTEM Prompt](05-troubleshooting/ollama-chat-template-pipe-stdin-bypass.md)
* [Claude Code Won't Log In (Warp & iTerm2) — Corrupt Keychain Credential](05-troubleshooting/claude-code-warp-login-corrupt-keychain-credential.md)