Skip to content Skip to sidebar

This is the technical support forum for WPML - the multilingual WordPress plugin.

Everyone can read, but only WPML clients can post here. WPML team is replying on the forum 6 days per week, 22 hours per day.

Sun Mon Tue Wed Thu Fri Sat
9:00 – 13:00 9:00 – 13:00 9:00 – 13:00 9:00 – 13:00 9:00 – 13:00 - -
14:00 – 18:00 14:00 – 18:00 14:00 – 18:00 14:00 – 18:00 14:00 – 18:00 - -

Supporter timezone: Asia/Yerevan (GMT+04:00)

Tagged: 

This topic contains 5 replies, has 0 voices.

Last updated by Christopher Amirian 1 month ago.

Assisted by: Christopher Amirian.

Author Posts
May 9, 2026 at 9:30 am #18024183

tysonO

Hello Support Team,

I am experiencing a stubborn issue where custom PHP snippets (such as a Free Shipping Bar and Volume Discount logic) are not displaying their translations on the frontend of my site, particularly during WooCommerce AJAX cart updates.

Environment:
- WordPress / WooCommerce
- Elementor
- WPML
- Server: CloudPanel (Varnish Cache)

The Issue:
The custom code strings are correctly wrapped in standard WordPress translation functions (e.g., `esc_html__( 'Text', 'woocommerce' );`). WPML has successfully scanned the strings, and they are marked as 100% translated (Status: Complete) in the WPML String Translation dashboard.

However, the frontend continues to display the default language (Dutch) instead of the translated language (e.g., German). This happens most notably when WooCommerce triggers an AJAX fragment refresh (e.g., updating the cart).

Troubleshooting Already Completed:
To save time, here is what we have already ruled out:

1. Caching is NOT the issue: We have completely purged Varnish, WP Super Cache, and Elementor caches. Testing with `?noCache=1` in an Incognito window still shows the wrong language.
2. Text Domains are standard: We moved the strings to the default `woocommerce` domain to ensure WPML recognizes them.
3. Source Language is correct: We verified the strings are registered with English as the source language in the database.
4. AJAX Language Switching: We added `do_action('wpml_switch_language', $lang)` to the `admin-ajax.php` handler and passed the language code via JavaScript, but the server still returns the default language fragment.
5. MO Files: We ran the WPML Troubleshooting clean-up tools and forced the recreation of the `.mo` files.

Despite the database showing the strings are translated, the WPML engine is failing to deliver the translated text to the frontend during render/AJAX.

Could you please advise on what might be blocking WPML from outputting these specific translated strings?

Thank you,

May 10, 2026 at 7:51 am #18024864

Christopher Amirian
WPML Supporter since 07/2020

Languages: English (English )

Timezone: Asia/Yerevan (GMT+04:00)

Hi,

Welcome to WPML support. We will do our best to help, but please consider that the issue is regarding a custom code, which is outside of our support scope.

What can I suggest at this stage:

1. Do not use woocommerce text domain

WordPress's __() / esc_html__() translate strings via .mo files for the named text domain. WPML String Translation does not intercept arbitrary __() calls and substitute its database translations into them. WPML's intercept mechanism for theme/plugin domains only finds strings that already exist in that theme/plugin's .mo file or that have been registered through the proper String Translation pathway.

Borrowing the woocommerce text domain for custom code is also a small i18n violation in itself — that text domain belongs to the WooCommerce plugin, and per WordPress.org's official internationalization guidelines a string's domain must match the plugin/theme that owns it (https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/). Mixing custom strings into another plugin's domain doesn't help WPML find them and can produce inconsistent results.

For custom code that you control (Free Shipping Bar, Volume Discount logic, snippets in functions.php or a child theme), use WPML's purpose-built hooks instead:

// 1) Register the string once (e.g. on init, or when the value is saved)
do_action( 'wpml_register_single_string', 'My Custom Snippets', 'free_shipping_bar_text', 'Free shipping over €50!' );

// 2) Output the translated string in your snippet
$text = apply_filters(
    'wpml_translate_single_string',
    'Free shipping over €50!',     // original value
    'My Custom Snippets',          // domain (must match #1)
    'free_shipping_bar_text'       // name (must match #1)
);
echo esc_html( $text );

After registering, the strings will appear in WPML → String Translation under the "My Custom Snippets" domain. and then translate them.

2. AJAX language context

WPML does have an option to store the context to "wp-wpml_current_language" cookie for Ajax calls, I see that you already have that option enabled, but worths the check:

- Go to "WordPress Dashboard > WPML > Languages > Language filtering for AJAX operations".
- Make sure that the "Store a language cookie to support language filtering for AJAX" option is checked.
- Click the "Save button".

For more information:
https://wpml.org/documentation/getting-started-guide/language-setup/enabling-language-cookie-to-support-ajax-filtering/

After enabling, please also remove the do_action('wpml_switch_language', $lang) call from your AJAX handler — it shouldn't be needed and depending on where it sits in the request lifecycle, it can mask the real cause.

3. Varnish on AJAX responses

You mentioned Varnish, and that you cleared it. Worth confirming separately: clearing Varnish purges currently-cached objects, but doesn't change how Varnish keys responses. If Varnish isn't varying its cache key by the wp-wpml_current_language cookie (or by language-bearing URL/header), it will keep serving the first AJAX response it cached for any given URL across all languages, even after a purge — because the second visitor's request will look identical to Varnish.

Test it on an environment that does not have Varish cache at all in a staging version and see if it works.

We will not be able to delve into your custom code to know what might be the problem cause, as this is considered outside of our support scope.

For more information:
https://wpml.org/purchase/support-policy/

Thanks.

May 10, 2026 at 9:04 am #18024928

tysonO

Hi WPML Support Team,

Thank you for your help and explanation earlier. I updated my code based on your instructions and used the method you recommended.

The translations now appear correctly inside the WPML String Translation area, and I translated them successfully.

However, the problem on the website itself is still there.

Even though the translations are saved in WPML, the website keeps showing the original English/Dutch text on all language versions.

I also tried to make sure this was not caused by caching:

* I completely turned off Varnish cache on the server.
* I tested in private/incognito windows.
* I cleared WordPress and Elementor cache/transients.

So at this point, it does not seem to be a cache problem.

It looks like WPML can save the translations, but for some reason it is not showing them on the frontend of the website.

I am using separate domains for each language (.nl, .de, .fr).

Could you please advise what else I should check or try?

Thank you,

May 11, 2026 at 10:21 am #18026943

Christopher Amirian
WPML Supporter since 07/2020

Languages: English (English )

Timezone: Asia/Yerevan (GMT+04:00)

Hello,

Thank you. There is no other thing that I can suggest. I will be happy to check that section of the code you added if you can use the CODE button in the ticket reply to paste so I can see if there is anything that I can pinpoint.

Thanks.

May 11, 2026 at 11:50 am #18027207

tysonO

Hi, Here is code of one of my snippets:

CODE:
if ( ! defined( 'ABSPATH' ) ) exit;

// ─── 1. WPML String Registration ──────────────────────────────────────────────
add_action( 'init', function() {
// This pushes the strings directly into the WPML database
do_action( 'wpml_register_single_string', 'RetaLabs Custom Snippets', 'fsb_unlocked', "You've unlocked free shipping!" );
do_action( 'wpml_register_single_string', 'RetaLabs Custom Snippets', 'fsb_add_more', 'Add %s more for free shipping' );
} );

// ─── 2. Region Thresholds ─────────────────────────────────────────────────────
function fsb_get_threshold() {
if ( ! function_exists('WC') || ! WC()->customer ) return 200;
$country = WC()->customer->get_shipping_country();
if ( ! $country ) $country = WC()->customer->get_billing_country();
$thresholds = [ 'NL' => 100 ];
return isset( $thresholds[ $country ] ) ? $thresholds[ $country ] : 200;
}

// ─── 3. Render Function ───────────────────────────────────────────────────────
function fsb_render( $context = '' ) {
if ( ! function_exists('WC') || ! WC()->cart ) return '';

$threshold = fsb_get_threshold();
$cart_total = floatval( WC()->cart->get_subtotal() );
$remaining = max( 0, $threshold - $cart_total );
$pct = min( 100, round( ( $cart_total / $threshold ) * 100 ) );
$sym = get_woocommerce_currency_symbol();
$qualified = $remaining <= 0;

$context_class = $context ? ' fsb--' . sanitize_html_class( $context ) : '';

// FETCH TRANSLATIONS VIA WPML API
$text_unlocked = apply_filters( 'wpml_translate_single_string', "You've unlocked free shipping!", 'RetaLabs Custom Snippets', 'fsb_unlocked' );
$text_add_more = apply_filters( 'wpml_translate_single_string', 'Add %s more for free shipping', 'RetaLabs Custom Snippets', 'fsb_add_more' );

ob_start(); ?>
<div class="fsb-wrap<?php echo $context_class; ?>">
<div class="fsb-label">
<?php if ( $qualified ) : ?>
<span class="fsb-msg fsb-msg--done">✦ <?php echo esc_html( $text_unlocked ); ?></span>
<?php else : ?>
<span class="fsb-msg">
<?php
printf(
esc_html( $text_add_more ),
'' . $sym . number_format( $remaining, 2 ) . ''
);
?>
</span>
<span class="fsb-thresh"><?php echo $sym . $threshold; ?></span>
<?php endif; ?>
</div>
<div class="fsb-track">
<div class="fsb-fill" style="width:<?php echo $pct; ?>%"></div>
</div>
</div>
<?php
return ob_get_clean();
}

// ─── 4. Styles ────────────────────────────────────────────────────────────────
function fsb_styles() {
if ( ! function_exists('WC') ) return;
echo '<style>
.fsb-wrap { background: #111; border: 0.5px solid #2a2a2a; border-radius: 10px; padding: 12px 16px; margin-bottom: 18px; font-family: inherit; box-sizing: border-box; }
.fsb-wrap.fsb--mini-cart { margin: 0; border-radius: 0; border-left: 0; border-right: 0; border-top: 0; padding: 10px 14px; }
.fsb-label { display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: #999; margin-bottom: 9px; gap: 8px; }
.fsb-label strong { color: #EEB700; font-weight: 600; }
.fsb-thresh { color: #EEB700; font-size: 12px; white-space: nowrap; opacity: 0.75; }
.fsb-msg--done { color: #EEB700; font-weight: 500; width: 100%; text-align: center; }
.fsb-track { background: #222; border-radius: 99px; height: 7px; overflow: hidden; }
.fsb-fill { height: 100%; border-radius: 99px; background: #EEB700; transition: width 0.5s ease; min-width: 4px; }
</style>';
}
add_action( 'wp_head', 'fsb_styles' );

// ─── 5. Hooks ─────────────────────────────────────────────────────────────────
add_action( 'woocommerce_before_cart', function() { echo fsb_render('cart'); }, 5 );
add_action( 'woocommerce_before_checkout_form', function() { echo fsb_render('checkout'); }, 5 );
add_action( 'woocommerce_before_mini_cart', function() { echo fsb_render('mini-cart'); }, 5 );
add_action( 'woocommerce_before_shop_loop', function() { echo fsb_render('shop'); }, 5 );
add_shortcode( 'free_shipping_bar', function() { return fsb_render('shortcode'); } );

add_filter( 'woocommerce_add_to_cart_fragments', function( $fragments ) {
$fragments['div.fsb-wrap.fsb--mini-cart'] = fsb_render('mini-cart');
return $fragments;
} );

// ─── 6. JS AJAX Trigger ───────────────────────────────────────────────────────
add_action('wp_footer', function() {
if (!function_exists('WC')) return;
?>
<script>
if (typeof jQuery !== 'undefined') {
jQuery(document.body).on('wc_fragments_refreshed added_to_cart removed_from_cart updated_cart_totals', function() {
var ajax_url = (typeof woocommerce_params !== 'undefined') ? woocommerce_params.ajax_url : '/wp-admin/admin-ajax.php';

jQuery.post(ajax_url, {
action: 'fsb_get_html',
nonce: '<?php echo wp_create_nonce("fsb_nonce"); ?>'
}, function(res) {
if (!res.success) return;
document.querySelectorAll('.fsb-wrap').forEach(function(el) {
var context = Array.from(el.classList).find(c => c.startsWith('fsb--'))?.replace('fsb--', '') || 'shortcode';
if (res.data[context]) {
el.outerHTML = res.data[context];
}
});
});
});
}
</script>
<?php
});

// ─── 7. AJAX handler (Cleaned up per WPML guidelines) ─────────────────────────
add_action('wp_ajax_fsb_get_html', 'fsb_ajax_get_html');
add_action('wp_ajax_nopriv_fsb_get_html', 'fsb_ajax_get_html');
function fsb_ajax_get_html() {
check_ajax_referer('fsb_nonce', 'nonce');

wp_send_json_success([
'cart' => fsb_render('cart'),
'checkout' => fsb_render('checkout'),
'mini-cart' => fsb_render('mini-cart'),
'shop' => fsb_render('shop'),
'shortcode' => fsb_render('shortcode'),
]);
}

May 12, 2026 at 7:49 am #18029332

Christopher Amirian
WPML Supporter since 07/2020

Languages: English (English )

Timezone: Asia/Yerevan (GMT+04:00)

Hello,

I checked the code and honestly it is correctly crafted. So the code is not the issue.

I would test on a minimal environment to see if the issue persists or not.

- IMPORTANT STEP! Create a backup of your website. Or better approach will be to test this on a copy/staging version of the website to avoid any disruption of a live website.
- Switch to the default theme such as "TwentyTwenty" by going to "WordPress Dashboard > Appearance > themes".
- Add the code directly to functions.php file of the default theme.
- Go to "WordPress Dashboard > Plugins" and deactivate all plugins except:
. WPML Multilingual CMS
. WPML String translation
. WooCommerce
. WooCommerce Multilingual
- Make sure that you scan the theme from WordPress Dashboard > WPML > Theme and plugins localization.
- Go to WordPress Dashboard > String Translation and select the "RetaLabs Custom Snippets" text domain and click the search button.
- Make sure all the strings are translated there.
- Check if you can still recreate the issue.
- If not, re-activate your plugins one by one and check the issue each time to find out the plugin that causes the problem.

Thanks.

The topic ‘[Closed] WPML translation failing on frontend for custom AJAX snippets (Strings are registered & translated)’ is closed to new replies.