Aryan Jasala

Translating a WordPress plugin, including the JavaScript

A globe wired to the three translation functions

Internationalisation is making a plugin translatable. Localisation is someone actually translating it. You do the first one, and if you do it wrong the second one is impossible. This covers text domains, the PHP functions, and the part that has changed since most tutorials were written: getting strings out of JavaScript.

The JavaScript translation chain
Plugin headerText Domain matches plugin slug
Register scriptdeclare wp-i18n dependency
Set translationshandle plus text domain
WordPress finds fileincluding translate.wordpress.org
__() in JavaScriptimported from @wordpress/i18n

Mismatch the text domain and the slug and translations never load, with no error to tell you why.

The text domain, the script dependency and the file lookup must all line up before __() in JavaScript returns a translation.

The text domain

A text domain is a unique string that tells WordPress which group of strings belongs to your plugin. It goes in the plugin header and it must match the plugin slug:

* Text Domain: my-basics-plugin
 * Domain Path: /languages

Mismatch the slug and translations from wordpress.org will never load, with no error to tell you why. ๐Ÿ™ƒ

The PHP functions

__( 'String', 'text-domain' );        // returns
_e( 'String', 'text-domain' );        // echoes
esc_html__( 'String', 'text-domain' ); // returns, escaped for HTML
esc_attr__( 'String', 'text-domain' ); // returns, escaped for an attribute

Two rules that matter more than the function list:

  • escape at output, including dates. A translated string is still untrusted input, because you did not write the translation
  • use placeholders, never string concatenation

Concatenating is the one that actively breaks translation, because word order differs by language and a translator handed half a sentence cannot fix it:

printf(
    /* translators: %s: movie title */
    esc_html__( 'Now showing: %s', 'movie-library' ),
    esc_html( $title )
);
Translations versus data

wp_set_script_translations

  • Ships translated strings to JavaScript
  • Needs wp-i18n as a script dependency
  • Strings come from __() in @wordpress/i18n
  • WordPress finds the file, including translate.wordpress.org

wp_localize_script

  • Passes data from PHP to JavaScript
  • Prints a JSON global before the script
  • For a REST URL, nonce or post ID
  • Not for translated strings

Older code uses wp_localize_script for translations because for years there was nothing better.

Both are still useful, but only wp_set_script_translations hooks into WordPress's translation infrastructure.

JavaScript, the current way

This is where old tutorials will send you wrong, including notes I wrote in 2022. ๐Ÿ“œ

For translating strings in JavaScript, the answer is wp_set_script_translations(). Import __ from the @wordpress/i18n package, which is how every block label gets translated, declare wp-i18n as a script dependency, and register the translations:

wp_register_script( 'movie-library-editor', $url, array( 'wp-i18n' ), $version );
wp_set_script_translations( 'movie-library-editor', 'movie-library' );

WordPress then works out whether a translation file exists for that handle, including from translate.wordpress.org. An optional third argument points it at a local directory instead. ๐Ÿ”

wp_localize_script is for data, not translations

wp_localize_script() still exists and is still useful, but its job is passing data from PHP to JavaScript. It was used for translations for years because there was nothing better, and that is why it is all over older code:

$data = array(
    'some_test_data' => __( 'Some random data', 'movie-library' ),
    'test_array'     => array( 'Some', 'Test', 'Data' ),
);

wp_localize_script( 'custom-image-gallery', 'test_data', $data );

Three arguments: the script handle, the name of the JavaScript object to create, and the array. It JSON-encodes the array and prints it as a global object before the script is enqueued, so test_data is available by the time your code runs.

Use it to hand JavaScript a REST URL, a nonce or a post ID. Do not use it to ship translated strings, because wp_set_script_translations() exists and hooks into the translation infrastructure that wp_localize_script knows nothing about. ๐Ÿคก

What this does not cover

Plural forms with _n(), context with _x(), generating .pot files with WP-CLI, and how a .json translation file for JavaScript gets named, which is its own small pile of rules. ๐Ÿ“š