Translations in JS Files

Localization in PHP files has long been a common practice for WordPress, but translation in JS files, which was implemented through the wp_localize_script() function, left much to be desired in terms of a more dynamic and convenient approach. With the release of WordPress 5.0, this capability has become available, and now it's possible to conveniently use localization functions like __(), _x() directly in JS files, similar to how it's done in PHP.

How It Works

You need to "tell" WP that your script has a translation, and specify which text domain (ID) should be used for this translation. This is done using the wp_set_script_translations() function.

So, the registration of your script with translation should look like this:

wp_register_script( 'my-handle', plugins_url( '/js/my-file.js', __FILE__ ) );
wp_set_script_translations( 'my-handle', 'my-domain' );

The wp_set_script_translations() function adds a dependency on the main script for wp-i18n and includes the .json file.

Be sure to familiarize yourself with the description of the wp_set_script_translations() function.

Now, in the JS code, you need to use the wp-18n object as follows:

const { __, _x, _n, _nx } = wp.i18n;

__( 'Hello', 'myl10n' );
_x( 'Hi', 'short word', 'myl10n' );
_n( '%s star', '%s stars', 5, 'myl10n' );
_nx( '%s star', '%s stars', 5, 'superstars', 'myl10n' );
sprintf( __( 'See Link: %s', 'myl10n' ), 'http://site.com' );

The translation functions used are completely similar to the similarly named PHP functions and are used in the same way. See __(), _n(), _x(), _nx().

The setup for translating JS files is ready!

The only thing left is to create the translation .json files from which the translated strings will be taken. Read below for how to do this.

Notes:
  • The absence of esc_html(), esc_html__(), etc. functions in JS is explained by the fact that they are not needed in JavaScript, as the browser can handle escaping unsafe characters by itself.

  • The content of the JSON translation file is output directly in the HTML page as JavaScript code, just before the script for which the translation is being included.

  • Although it is not recommended to use HTML in translated strings, sometimes it is necessary, for example, to add links: Check <a href="%s">my website</a>.. Currently, this is not so easy to do without using innerHTML (which can be dangerous). This situation is discussed on GitHub.

  • If multiple scripts are registered for which translations are needed, then translations need to be included for each script separately (each should have its own JSON translation file):

    wp_register_script( 'my-plugin-script', plugins_url( 'js/my-script.js', __FILE__ ), [ 'wp-i18n' ], '0.0.1' );
    wp_set_script_translations( 'my-plugin-script', 'my-plugin' );
    
    wp_register_script( 'my-awesome-block', plugins_url( 'js/my-block.js', __FILE__ ), [ 'wp-i18n' ], '0.0.1' );
    wp_set_script_translations( 'my-awesome-block', 'my-plugin' );

    This approach is needed for optimization: only the translation strings used in the included script are added to the HTML - nothing extra.

Creating a Translation JSON File

Option 1: Creating with wp-cli (recommended)

The first thing you need for this method is to have a ready-made PO file, in which, in addition to translations for PHP files, there are also translations for JS files. Usually, to include translations from JS files in the base PO file, nothing extra needs to be done; everything is done as usual, and in JS files, __(), _x() functions are used, which are automatically picked up by programs like Poedit or other parsers, for example, Loco Translate plugin.

Let's assume that in our plugin there is a folder languages and in it there are several PO files with translations for different languages, which include strings from JS files.

Then, to create .json files from existing .po files, you can use the wp i18n make-json command.

wp i18n make-json languages --no-purge
  • languages — the path relative to your theme/plugin to the folder where the .po files are located. The .json files will appear in the same folder. If you need the .json files to be created in a different location, specify the path as the second parameter of the command.
  • --no-purge — a flag that instructs not to delete translation strings from the PO file. By default, they are deleted (not clear why this is necessary by default).
How the MD5 hash is created

When creating a .json file in this way, it will have the following format: {domain}-{locale}-{md5}.json, where the md5 hash will be created from the path to the file taken from the PO file.

For example, in the PO file, a translation string looks like this:

#: scripts.js:9
msgid "Hello"
msgstr "Привет"

Then the md5 will be created from the string: md5('scripts.js'). This method of creating a hash coincides with the one used by default in WP when using the function to include the json file wp_set_script_translations().

Multiple json files: if the PO file contains translations for multiple .js files, multiple .json files will be created for each of them.

No extra strings: only translation strings related to a specific js file will be included in the created json file.

Option 2: Convert using gulp

Step 1. Create a .po file

It will contain strings from the JS file and their translation. How to create PO files is described in the article about translations. Here, everything is done in the same way, only to find translation strings, you need to specify our js file.

Let's consider an example. Suppose we need to create a .po file for the JS file scripts.js, which is located in the theme's root.

Create the file /languages/js/myl10n-ru_RU-my-script.po in the theme and add the following code to it (for clarity, make the file name similar to the future json file):

msgid ""
msgstr ""
"Project-Id-Version: my-plugin-name\n"
"POT-Creation-Date: 2020-04-28 12:17+0500\n"
"PO-Revision-Date: \n"
"Last-Translator: Kama <[email protected]>\n"
"Language-Team: WP Kama\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;"
"_nx_noop:1,2,3c;esc_attr__;esc_attr_e;esc_html__;esc_html_e\n"
"X-Generator: Poedit 2.3\n"
"X-Poedit-Basepath: ../..\n"
"X-Poedit-SearchPath-0: scripts.js\n"
"X-Poedit-SearchPathExcluded-0: node_modules\n"

Pay attention to the lines:

  • "X-Poedit-Basepath: ../..\n" — here you need to specify the path from the PO file to the theme folder. Since our file is located in two folders /languages/js, we go up two folders.
  • "X-Poedit-SearchPath-0: scripts.js\n" — here you need to specify the path to the js file relative to the theme folder (the base path, Basepath). Since our file is right in the theme, just specify the file name here.

Now open the Poedit program, drag our PO file there, click "Update from code", translate the received strings, and click "save".

The PO file is ready!

Step 2. Convert the .po file to .json

Please note that the json file will include all strings from the PO file (not just js).

To do this, we need the po2json and jed libraries, because the conversion needs to be done in the jed format. Install them in the project with the following commands:

npm install -D po2json
npm install -D jed

Add the following code to the gulpfile.js:

let po2json = require('po2json'),
	Jed     = require('jed'),
	Write   = require('write');

gulp.task( 'po2json', async ()=>{

	// path to the PO file
	let PO_file = 'languages/js/myl10n-ru_RU-my-script.po'

	po2json.parseFile( PO_file, { format:'jed' }, ( err, jsonData )=>{

		let i18n = new Jed( jsonData )
		let JSON_file = PO_file.replace( '.po', '.json' )

		Write.sync( JSON_file, JSON.stringify( i18n, null, 2 ), { newline: true } )
	})

} )

In this code, you need to change the name of the created json file to fit your case.

Now, create the json file with this command:

gulp po2json

As a result, a .json file should appear next to the .po file.

The procedure for creating a JSON file can be set to a watcher so that when the PO file changes, the JSON file changes automatically.

Option 3: Publishing a plugin/theme in the WordPress directory

In this case, everything is done automatically: all JS files will be automatically analyzed just like it is already done with PHP files. All discovered translations will be added to translate.wordpress.org to allow the community to make translations for them.

Thus, the .json translation files are created automatically. It is only necessary to specify the parameters Text Domain: and Domain Path: — the translation domain and the path to the translation folder in the plugin headers, and then translate the obtained strings on the GlotPress site (translate.wordpress.org).

WP-CLI i18n commands are used for parsing translation strings for JS on GlotPress. This method is more stable compared to makepot.php.

Name of the Translation JSON File

Let's consider what name the JSON translation file should have so that the wp_set_script_translations() function finds the corresponding file when attempting to include it.

There are two options: when the third parameter $path is specified and when it is not:

  • NOT specified third parameter $path:

    /wp-content/languages/themes/{domain}-{locale}-{md5}.json  // plugin-ru_RU-db8f629adc6c4c33f29613cfb71a6038.json
  • Specified third parameter $path:
    SPECIFIED_PATH/{domain}-{locale}-{handle}.json             // plugin-ru_RU-script.json
    SPECIFIED_PATH/{domain}-{locale}-{md5}.json                // plugin-ru_RU-db8f629adc6c4c33f29613cfb71a6038.json
    /wp-content/languages/themes/{domain}-{locale}-{md5}.json  // plugin-ru_RU-db8f629adc6c4c33f29613cfb71a6038.json

The MD5 hash in the name is formed from the path to the file relative to the plugin/theme folder. For more details, see here.

Hooks

WordPress provides filters such as load_textdomain and gettext that allow overriding the path to translation files or individual translations.

In WordPress 5.0.2, the following filters were added to filter the behavior of wp_set_script_translations() so that the same can be done for JavaScript translations. The following filters are available: