Translation improvements in WP 6.5 (.l10n.php)

In WordPress 6.5, the internal translation loading system was redesigned. The main goal of the change is to speed up localized sites and reduce memory usage.

Previously WordPress mainly worked with binary .mo files. Starting with WP 6.5, the core can use the new PHP-format translations - .l10n.php.

At the same time, backward compatibility remained: old .po and .mo files continue to work.

How it was

Translations were stored in two files:

  • .po - the original editable translation file.
  • .mo - binary file that WordPress loads.

When loading a text-domain, WordPress found the needed .mo file, parsed it, and saved translations in the global variable $l10n.

Main downsides of the old logic:

  • .mo - binary format, it needs to be parsed on load.
  • On sites with a large number of translations, this could noticeably affect speed.
  • Locale switching worked slower.

What happened (WP 6.5)

In WordPress 6.5 a new localization library appeared, based on the plugin Performant Translations.

It improves translation work in several places at once:

  • loads .mo files faster;
  • uses less memory;
  • supports simultaneous loading of several locales;
  • speeds up locale switching;
  • supports new format .l10n.php;
  • uses OPcache for translation PHP files.

The main difference is that WordPress now prefers .l10n.php if such a file exists next to the .mo.

New format .l10n.php

The file .l10n.php is a PHP file that returns an array with translations.

Example:

<?php
return array(
	'project-id-version' => 'WordPress - 6.5.x',
	'report-msgid-bugs-to' => '[email protected]',
	'messages' => array(
		'Original string' => 'Translated string',
		'context' . "\4" . 'Original string' => 'Translated string with context',
		'One product' => 'One product' . "\0" . '%s items' . "\0" . '%s items',
		'context' . "\4" . 'One product with context' => 'One product' . "\0" . '%s items' . "\0" . '%s items',
	),
);

Format features:

  • key of the array - original string;
  • value - translation;
  • context is separated by the character "\4";
  • plural forms are joined via "\0";
  • the file returns a ready array.
  • the file is cached by OPcache.

How WordPress selects a translation file

Simplified, the logic is as follows:

  1. WordPress determines the text domain and locale.
  2. It finds the path to the .mo file.
  3. It checks the preferred translation format (default format - php).
  4. If there is a suitable .l10n.php nearby, it is loaded.
  5. If there is no .l10n.php, .mo is used.

So the new logic does not break old translations.

Example:

wp-content/languages/plugins/my-plugin-ru_RU.mo
wp-content/languages/plugins/my-plugin-ru_RU.l10n.php

If both files exist, WordPress will load:

my-plugin-ru_RU.l10n.php

If the PHP file is not present, the following will be loaded:

my-plugin-ru_RU.mo

What changed for developers

For a normal plugin or theme, almost nothing needs to be changed.

Translation functions remain the same:

__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );
_x( 'Text', 'context', 'my-plugin' );
_n( 'One item', '%s items', $count, 'my-plugin' );

Only the internal method of loading translation files changes.

If a plugin or theme is hosted on WordPress.org, language packs may automatically contain .l10n.php files.

If translations are stored locally or not provided via WordPress.org, PHP files can be created manually.

Generating .l10n.php via WP-CLI

WP-CLI can create PHP translation files from .po files.

Create PHP files for all .po files in the current directory:

wp i18n make-php .

Create a PHP file from a single .po file and place the result in the languages directory:

wp i18n make-php example-plugin-ru_RU.po languages

After that, alongside the .po and .mo file, you will see:

example-plugin-ru_RU.l10n.php

Generating .l10n.php via PHP

To work with translation files in WP 6.5, the class WP_Translation_File appeared.

Example of converting a .mo file to a PHP file:

$contents = WP_Translation_File::transform( $mofile, 'php' );
if ( $contents ) {
	file_put_contents( $php_file, $contents );
}

Performant Translations Plugin

The plugin Performant Translations is no longer required for core logic, because much of its functionality has been built into the core.

But it is still useful if:

  • translations do not come from WordPress.org;
  • commercial plugins are used;
  • translations are stored only locally on the server;
  • you need to automatically create .l10n.php files from .mo.

The plugin can itself convert .mo files to the new PHP format if the corresponding .l10n.php file does not yet exist.

Filter translation_file_format

The filter translation_file_format allows changing the preferred translation file format.

By default, WordPress prefers php.

If you want to disable the use of .l10n.php and always use .mo:

add_filter( 'translation_file_format', static fn() => 'mo' );

Filter load_translation_file

The filter load_translation_file allows changing the translation file path before loading.

Unlike the old load_textdomain_mofile, this filter works not only with .mo, but also with .l10n.php.

Example:

add_filter(
	'load_translation_file',
	static function ( $file, $domain, $locale ) {
		if ( 'my-plugin' !== $domain ) {
			return $file;
		}

		$custom_file = WP_LANG_DIR . "/my-plugin/my-plugin-{$locale}.l10n.php";

		if ( file_exists( $custom_file ) ) {
			return $custom_file;
		}

		return $file;
	},
	10,
	3
);

Filter load_textdomain_mofile

The filter load_textdomain_mofile remains for backward compatibility.

It only works with the path to the .mo file:

add_filter(
	'load_textdomain_mofile',
	static function ( $mofile, $domain ) {
		if ( 'my-plugin' !== $domain ) {
			return $mofile;
		}

		return WP_LANG_DIR . '/my-plugin/custom-ru_RU.mo';
	},
	10,
	2
);

It is better to use load_translation_file when you need to work with the new PHP format.

Global variable $l10n

Previously WordPress stored loaded translations in $l10n as an object of class MO.

In WordPress 6.5 a new class WP_Translations is used, which mimics the old behavior.

For ordinary code this does not matter.

You need to check only projects that directly work with:

  • $GLOBALS['l10n'];
  • the MO class;
  • the internal structure of loaded translations.

Ordinary calls to __(), _e(), _x(), _n() do not need to change.

Caching the list of translation files

In WordPress 6.5, the search for translation files was also improved.

Previously WordPress could directly scan directories via glob(), for example when working with:

On sites with a large number of language files, this could be a costly operation.

Now the list of found files is cached in the object cache in the translations group.

The cache is cleared when updating language packs.

WordPress also looks for not only .mo, but also .l10n.php files.

Summary

For a plugin or theme developer, the new logic looks like this:

  • translation functions do not change;
  • text domain does not change;
  • .po and .mo remain working;
  • .l10n.php is used automatically if it exists;
  • for WordPress.org projects PHP files can come in language packs;
  • for local and commercial projects .l10n.php can be generated via WP-CLI or PHP;
  • to change the file path, it is better to use load_translation_file;

--