WPML
Status
Open
Reported for
WPML Multilingual CMS 5.0
Topic tags
Compatibility

Overview of the issue

Since WPML 5.0, language switches nest. Every language code opens a new scope, and only null closes one.

Plugins written for WPML 4.x often switch back by naming the language they saved:

$current = apply_filters( 'wpml_current_language', null );

do_action( 'wpml_switch_language', 'de' );
// ...
do_action( 'wpml_switch_language', $current ); // Meant as "and back".

WPML 4.x tolerated this. It kept one language slot, so the current language ended up right and the unbalanced switch stayed out of sight. It was never a correct way to close a switch. WPML 5.0 no longer absorbs it: the last call opens a second scope instead of closing the first one, and WPML keeps reporting that the language is switched.

WPML cannot detect this, because the same call is also a valid switch into the language the surrounding code came from.

The symptom is deferred. Right after the pair the current language is correct, so a test that checks only that passes. The next correct restore around you then closes your open scope instead of its own, and that code runs in the wrong language. Emails, REST responses and admin screens are where this shows.

A switch with no restore, or with an empty language code, leaves a scope open the same way.

Workaround

Close every scope with null, in a finally block:

do_action( 'wpml_switch_language', 'de' );

try {
	// ...your work in German...
} finally {
	do_action( 'wpml_switch_language', null ); // Closes the scope above.
}

Support WPML 4.x and 5.0 in one release

Copy these helpers in and rename the prefix:

// Opens a scope. Returns the value for the close helper, or false if none opened.
function myplugin_wpml_open_language_switch( $language_code ) {
	// '', false and 0 open a scope without changing the language. Only null closes one.
	if ( ! $language_code ) {
		return false;
	}
	$previous = apply_filters( 'wpml_current_language', '' );
	do_action( 'wpml_switch_language', $language_code );
	return $previous;
}

// Closes the scope the open helper opened. WPML 4.x has no stack: there null
// means "the language before the FIRST switch", so restore by name.
function myplugin_wpml_close_language_switch( $previous ) {
	if ( ! $previous ) {
		return;
	}
	$has_stack = defined( 'ICL_SITEPRESS_VERSION' )
		&& version_compare( ICL_SITEPRESS_VERSION, '5.0', '>=' );
	do_action( 'wpml_switch_language', $has_stack ? null : $previous );
}

Each call site then becomes:

$previous = myplugin_wpml_open_language_switch( 'de' );
try {
	// ... work ...
} finally {
	myplugin_wpml_close_language_switch( $previous );
}

Run grep -rn "wpml_switch_language" . to find every call site. Each one needs a matching close. Never restore by naming the language, and never switch on a code that can be empty.

The email hooks nest too: call wpml_restore_language_from_email once for every wpml_switch_language_for_email.

All known issues →