Internationalization improvements in 6.7

Various internationalization (i18n) improvements are in WordPress 6.7, and this developers note focuses on these.

Determining whether a translationtranslation The process (or result) of changing text, words, and display formatting to support another language. Also see localization, internationalization. exists

Sometimes it can be useful to know whether a translation already exists for in memory without having to first load the translation for the given text domain. The new has_translation() function allows for exactly that.

See #52696 / [59029] for more details.

Sending emails in the adminadmin (and super admin)’s localeLocale A locale is a combination of language and regional dialect. Usually locales correspond to countries, as is the case with Portuguese (Portugal) and Portuguese (Brazil). Other examples of locales include Canadian English and U.S. English.

Back in version 4.7, WordPress added the ability for users to set their preferred locale. When sending emails to a user, they are always sent in that locale, and everyone else receives the email in the site’s locale.

Now, WordPress 6.7 is going a step further: every time an email is sent to the administrator email address (admin_email), WordPress checks if a user with the same email address exists. If so, the email is sent in that user’s locale.

See #61518 / [59128] for more details.

Warnings if translations are loaded too early

WordPress now warns developers if they are loading translations too early in a pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party or theme, before the current user is known. Existing functions like load_plugin_textdomain() and load_theme_textdomain() were updated to defer the actual loading to the existing just-in-time translation logic in coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress.. This reduces the likelihood of triggering the warning, and even improves performance in some situations by avoiding loading translations if they are not needed on a given page.

Reminder: if your plugin or theme is hosted on WordPress.orgWordPress.org The community site where WordPress code is created and shared by the users. This is where you can download the source code for WordPress core, plugins and themes as well as the central location for community conversations and organization. https://wordpress.org/ and is still using load_*_textdomain(), you can remove this call. Since WordPress 4.6, plugins and themes no longer need load_plugin_textdomain() or load_theme_textdomain(). WordPress automatically loads the translations for you when needed. If you still support older WordPress versions or do not host your plugin/theme on WordPress.org, move the function call to a later hook such as init.

When attempting to load translations before after_setup_theme or init, WordPress tries to load the current user earlier than usual, without giving other plugins a chance to potentially hook into the process. It also prevents any plugins from filtering translation calls, e.g. for switching the locale or file location. Hence the addition of this warning to call out this unexpected behavior.

See #44937, [59127], and [59157] for more details.

Your plugin might be _doing_it_wrong() if you for example directly call get_plugin_data() (which attempts to load translations by default) or __() without waiting for the init hook. Here is a common example that was found in an actual plugin that was fixed:

/**
 * Plugin Name: Awesome Plugin
 */

function myplugin_get_version() {
	require_once ABSPATH . 'wp-admin/includes/plugin.php';
	// Prevent early translation call by setting $translate to false.
	$plugin_data = get_plugin_data( __FILE__, false, /* $translate */ false );
	return $plugin_data['Version'];
}

define( 'MYPLUGIN_VERSION', myplugin_get_version() );

If you do not explicitly set $translate set to false when calling get_plugin_data(), the function translates the plugin metadata by default. Since this plugin just needs the version number, there is no need for translating any of the other fields.

Another example:

/**
 * Plugin Name: Awesome Plugin
 */

class My_Awesome_Plugin {
	public $name;
	public function __construct() {
		// This triggers just-in-time translation loading
		$this->name = __( 'My Awesome Plugin', 'my-awesome-plugin' );

		// ... do something
	}
}

// This immediately instantiates the class, way before `init`.
$myplugin = new My_Awesome_Plugin();

Here, a class is immediately instantiated in the main plugin file, and code within the class constructor uses a translation function. This can be avoided by deferring the class instantiation until after init, or deferring the translation call until later when it is actually needed.

If your plugin is affected by this warning, you can use code like follows to find out the code path that triggers it:

add_action(
	'doing_it_wrong_run',
	static function ( $function_name ) {
		if ( '_load_textdomain_just_in_time' === $function_name ) {
			debug_print_backtrace();
		}
	}
);

Developer tools such as Query Monitor are also helpful in situations like this.


Props to @ocean90, @fabiankaegy for review.

#6-7, #dev-notes, #dev-notes-6-7, #i18n