Free WordPress Audit
13 March 2026
Troubleshooting

How I Fixed a 4-Year-Old JetMenu + WPML Bug That Breaks Multilingual Mega Menus

How I Fixed a 4-Year-Old JetMenu + WPML Bug That Breaks Multilingual Mega Menus

Table of content

If you’re using JetMenu (Crocoblock) with WPML on a multilingual WordPress site, you may have encountered this maddening problem: non-logged-in visitors see menu content in the wrong language. Not consistently — just often enough that clients send you worried screenshots.

This bug has been reported since 2020. Crocoblock knows about it (GitHub issue #1842, #1357). WPML knows about it (their forums). Yet four years later, there’s still no official fix.

Here’s what I found after hours of debugging, and the production-ready mu-plugin that solves it.

The Symptom

On a bilingual WordPress site (French as default language, English at /en/) using WPML in directory mode, the JetMenu mega menus were showing French content on English pages for anonymous visitors. The top-level navigation items (“IT Services”, “IT Solutions”) were correctly translated — but the mega menu dropdown panels, built with Elementor templates, always displayed French text like “Services gérés TI” instead of “Managed IT services”.

The tricky part: everything worked perfectly when logged into WordPress. The bug only affected non-logged-in visitors, making it hard to reproduce during development.

Why Standard Approaches Don’t Work

Before finding the real fix, I went through every approach I could think of:

Clearing Elementor cache — no effect, because JetMenu doesn’t use Elementor’s standard rendering pipeline for mega menus.

Using Elementor hooks like elementor/frontend/builder_content_data — these fire for headers, footers, and page content, but never for mega menu templates. I confirmed this with diagnostic logging. JetMenu has its own rendering system.

WPML cookie-based detection — unreliable because CDN warmup bots (NitroPack, WP Rocket, etc.) carry cookies between requests and contaminate language detection.

The Root Cause: Three Layers of Cache Gone Wrong

This isn’t a single bug. It’s three caching systems interacting badly.

Layer 1: JetMenu’s Transient Cache

After digging into JetMenu’s source code (jet-menu/includes/render/walkers/mega-menu-walker.php), I found the culprit. JetMenu caches the fully rendered HTML of each mega menu template in WordPress transients. The cache key is:

md5('jet_menu_elementor_template_data_' + template_id)

The critical flaw: this cache key doesn’t include the current language. Whichever language renders the mega menu first gets cached and served to all subsequent visitors, regardless of their language. If a French-speaking visitor (or a warmup bot) hits the page first, everyone gets French.

Layer 2: JetMenu’s Proprietary Rendering Pipeline

JetMenu does NOT use Elementor’s standard get_builder_content() method. It has its own walker that:

  1. Checks its transient cache for the template
  2. If cached, serves the HTML directly — Elementor is never called
  3. If not cached, calls Elementor’s rendering which reads _elementor_data from wp_postmeta
  4. Stores the rendered HTML in the transient for next time

Since step 2 skips Elementor entirely, hooks on Elementor’s rendering pipeline never fire for cached menus. This is why the standard approach of filtering Elementor’s output doesn’t work.

Layer 3: CDN Page Caching

If you use NitroPack, WP Rocket, or any CDN with full-page caching, there’s an additional layer. The CDN caches the complete HTML response — including the wrong-language mega menu — and serves it to all visitors before PHP even executes. The warmup bot can also propagate incorrect language cookies between requests, further contaminating the cache.

The Solution: A WordPress MU-Plugin

After several iterations (v1 through v5), I built a mu-plugin that attacks the problem at three levels. Place this file at /wp-content/mu-plugins/jetmenu-wpml-language-fix.php.

Step 1 — Clear JetMenu’s Transient Cache

On first run after deployment, the plugin deletes all JetMenu-related transients:

// One-time cleanup after deployment
$cleared = get_option( '_jmfix_cache_cleared_v5', false );
if ( ! $cleared ) {
    $template_ids = array( 295, 542, 546, 3661, 5201, 5208, 5215, 5219 );
    foreach ( $template_ids as $tid ) {
        $key = md5( sprintf( 'jet_menu_elementor_template_data_%s', $tid ) );
        delete_transient( $key );
    }
    // Also clean up any remaining jet-menu transients via SQL
    global $wpdb;
    $wpdb->query(
        "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient%jet_menu%'"
    );
    update_option( '_jmfix_cache_cleared_v5', time() );
}

Replace the template IDs with your own. You can find them in Elementor → My Templates — the post ID is in the URL when editing a template (post=295 means ID 295). Include both the original language and WPML translation IDs.

Step 2 — Disable JetMenu’s Template Cache

Since the cache doesn’t vary by language, it needs to stay off:

add_filter( 'option_jet_menu_options', function( $options ) {
    if ( is_array( $options ) ) {
        $options['use-template-cache'] = 'false';
    }
    return $options;
} );

Performance note: This sounds risky, but the impact is negligible if you use a page-level caching plugin. NitroPack/WP Rocket still caches the complete page at the CDN level. The PHP rendering only happens when the CDN cache is being built — not on every visitor request.

Step 3 — Redirect Elementor Meta Reads (The Core Fix)

This is the heart of the solution. When JetMenu renders a French template on an English page, we intercept the get_post_metadata calls and redirect them to the English template:

add_filter( 'get_post_metadata', function( $value, $object_id, $meta_key, $single ) {
    // Prevent infinite recursion
    static $is_redirecting = false;
    if ( $is_redirecting ) return $value;

    // FR template ID => EN template ID
    static $map = array( 295 => 5201, 542 => 5208, 546 => 5215, 3661 => 5219 );
    if ( ! isset( $map[ $object_id ] ) ) return $value;

    // Only on English pages — detect by URI, NOT cookies
    $uri = $_SERVER['REQUEST_URI'] ?? '';
    $is_en = ( strpos( $uri, '/en/' ) === 0
            || strpos( $uri, '/en?' ) === 0
            || $uri === '/en' );
    if ( ! $is_en ) return $value;

    // Only intercept Elementor meta keys
    static $el_keys = array(
        '_elementor_data' => 1,
        '_elementor_page_settings' => 1,
        '_elementor_edit_mode' => 1,
        '_elementor_css' => 1,
        '_elementor_page_assets' => 1,
    );
    if ( ! isset( $el_keys[ $meta_key ] ) ) return $value;

    // Redirect to English template data
    $en_id = $map[ $object_id ];
    $is_redirecting = true;
    $en_val = get_post_meta( $en_id, $meta_key, $single );
    $is_redirecting = false;

    return $single ? array( $en_val ) : $en_val;
}, 1, 4 );

A few important details in this code:

URI-based language detection instead of cookies or WPML functions. This is intentional — CDN warmup bots carry stale cookies between requests, which would cause incorrect language detection.

The $is_redirecting flag prevents infinite recursion, since get_post_meta() triggers the same get_post_metadata filter.

The $single parameter handling — when WordPress calls get_post_meta() with $single = true, the get_post_metadata filter must return array( $value ) (wrapped in an array), not just $value. This is a subtle WordPress API requirement that will silently break things if you get it wrong.

Adapting This to Your Site

To use this fix on your own site, you’ll need to change three things:

  1. The template ID mapping ($map) — replace with your French → English mega menu template IDs
  2. The language URI detection — adjust if your secondary language isn’t at /en/
  3. The template IDs in Step 1 — include all your mega menu templates (both languages)

Why a MU-Plugin?

The fix lives in /wp-content/mu-plugins/, which is completely independent from the plugins directory. When you update JetMenu, Elementor, or WPML, WordPress only replaces files in /wp-content/plugins/ — it never touches mu-plugins. Your fix survives every update.

The only thing to watch for: if a future JetMenu update changes its internal cache key (jet_menu_elementor_template_data_) or its rendering pipeline, the fix could become ineffective. I recommend checking the mega menus on your secondary language after each JetMenu update, just to be safe.

After Deploying

Don’t forget to purge your CDN cache (NitroPack, WP Rocket, etc.) after deploying the mu-plugin. Otherwise, visitors will keep getting old cached pages with the wrong-language menus until the cache expires naturally.

Wrapping Up

This bug cost me hours of diagnostic work. Four years of user reports on GitHub, scattered forum threads, and no official resolution from Crocoblock. The key insight was understanding that JetMenu’s template cache, its proprietary rendering pipeline, and CDN page caching were all working against multilingual setups simultaneously.

The fix is running in production without issues. If you’re dealing with the same problem and need help implementing it, feel free to get in touch. I’ve already done the research — you don’t have to redo it.


Annie Bergeron — WordPress Expert, Quebec — 15+ years building, optimizing, and maintaining WordPress sites for businesses across Quebec.