majorwiki/05-troubleshooting/wordpress-67-textdomain-just-in-time-notice.md
majorlinux e767ebffcb Add runbook: WordPress 6.7 _load_textdomain_just_in_time notice
Covers the WP 6.7 doing_it_wrong notice fired when a theme/plugin
translates before init (e.g. nav-menu labels on after_setup_theme).
Documents source fix (defer to init) and the update-safe mu-plugin
suppression via doing_it_wrong_trigger_error, plus the renamed-theme
domain gotcha. Derived from the majorlinux.com kappa/marstheme triage.
2026-06-21 11:44:48 -04:00

8.8 KiB

title domain category tags status created updated
WordPress 6.7 _load_textdomain_just_in_time Notice (Theme/Plugin Loads Translations Too Early) troubleshooting troubleshooting
wordpress
wordpress-6.7
php
i18n
textdomain
theme
mu-plugin
deprecation
troubleshooting
published 2026-06-21 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.:

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:

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

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 availableupdate 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)

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:

// 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
/**
 * 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

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:

// 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