i18n In JavaScript

This article is part of the WordPress guide. Read the introduction.

Table of Contents

We’ve covered PHP, but what about strings in JavaScript? There are 2 ways to internationalize your js, and we’ll start with the worse one.

wp_localize_script()

If you want to support WordPress <5.0, you have to use the wp_localize_script() function. This function accepts an associative array and creates a corresponding JavaScript object. You also need to pass it a handle (of your enqueued JS file) and the name for the object. The result is inline js rendered just before your script.

Note that you are in complete control over the passed array, which also means that you’re solely responsible for making it contain all of the translations and using appropriate key names. You are not limited to any structure whatsoever. As a matter of fact, we’ve already mentioned this function before when discussing passing any arbitrary data from PHP to JS.

A standard way is to just hard-code all the keys in the array and make the values use the PHP translation system. This way, you can translate the strings in the PO files, even though they will eventually be used in JavaScript. Here’s what I mean:

PHP
wp_register_script( 'my-plugin', plugins_url( 'my-plugin.js', __FILE__ ), [], '1.0', true );

wp_localize_script( 'my-plugin', 'myPluginL10n', [
    'hello_world' => __( 'Hello world', 'my-plugin' ),
    'goodbye' => __( 'Goodbye!', 'my-plugin' ),
] );

The strings “Hello world” and “Goodbye!” will be translated on the server before the JS object is created. Then, in your JavaScript files, you’d have to remember to always use strings from this localization object:

JavaScript
console.log( myPluginL10n.hello_world );

JSON Files

WordPress 5.0 marked a new era of WordPress – the introduction of the Block Editor. Along with that came the beginning of a shift from PHP to JavaScript. The old and brittle translation system for JS was no longer a viable option, which is why core WordPress developers created the @wordpress/i18n package.

This package brings all of the most important i18n functions from PHP straight into JavaScript. This includes:

  • __()
  • _x()
  • _n()
  • _nx()
  • sprintf()

Note the lack of _e(), as there’s no echo in JS. Also note sprintf(), which is a native PHP function, but is so fundamental to the way the translation system works that it was included in this package. All of those functions have exactly the same signature and work exactly the same as their PHP counterparts.

Let’s create an i18n-test.js file and load it in our plugin. Here’s its code:

JavaScript
const { __, _x, _n, sprintf } = wp.i18n;
console.log( __( 'Hello, World!', 'post-reading-time' ) );

When loading a translated JS file, you have to specify a “wp-i18n” dependency. This is just a file handle of the JS file containing the package’s code. You also have to call the wp_set_script_translations() function. This function expects, in order:

  • The file handle of the enqueued script.
  • The text domain.
  • The full path to the directory containing your l10n files.
PHP
function pgn_load_js() {
    wp_enqueue_script(
        'pgn-i18n-test',
        plugins_url( 'i18n-test.js', __FILE__ ),
        array( 'wp-i18n' ),
        filemtime( __DIR__ . '/i18n-test.js' )	// auto-versioning
    );
    wp_set_script_translations( 'pgn-i18n-test', 'post-reading-time', __DIR__ . '/languages' );
}
add_action( 'wp_enqueue_scripts', 'pgn_load_js' );

That’s great, but how are these strings actually translated? Well, that’s the interesting part. The WP-CLI command for generating the POT file parses JavaScript files as well. This means that all the __() calls in your JS files will be included in the POT file, among your typical PHP strings.

Because it’s all in the exact same file, localization looks exactly the same. You just translate the string in the PO file. Here’s the addition to our Polish translation:

Bash
#: i18n-test.js:2
msgid "Hello, World!"
msgstr "Witaj, Świecie!"

That’s where similarities end. See, the JS translations do not use MO files. They use JSON files. More specifically, WordPress requires one Jed-formatted JSON file per JavaScript source file. Let me say that again. Every single one of your JS files will have its own translation JSON file. MO files are only for PHP.

These JSON files are the counterparts to MO files. You do not create or touch them directly. There’s a new command specifically for generating these JSON files: “wp i18n make-json”. You run this command after you’ve translated all your strings in the PO file. We’ll run it like that:

Bash
wp i18n make-json languages languages

The first “languages” is the source directory, and the second “languages” (the same in this case, but that’s not necessarily the rule) is the destination directory. The source directory is the directory containing your PO files. Here’s the name of the file that’ll be generated as a result of running this command:

post-reading-time-pl_PL-2879eac789a3f955c9bf014659d15893.json

And here is its content:

JSON
{
  "translation-revision-date": "YEAR-MO-DA HO:MI+ZONE",
  "generator": "WP-CLI/2.12.0",
  "source": "i18n-test.js",
  "domain": "messages",
  "locale_data": {
    "messages": {
      "": {
        "domain": "messages",
        "lang": "en",
        "plural-forms": "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
      },
      "Hello, World!": [
        "Witaj, Świecie!"
      ]
    }
  }
}

Okay, let’s start with the obvious. What the hell is this filename? The answer is quite simple – it’s the md5 hash of its script’s filename. Think about it for a second. Every JSON file corresponds to precisely one JS file. They have to be linked together somehow, otherwise WordPress won’t be able to find and load the appropriate JSON file. Using the filename itself is an option, but an even more robust approach is using its hash. Therefore, the JSON’s filename format is: {text-domain}-{locale}-{md5_hash}.json

You shouldn’t be very concerned with the JSON file itself. You’re never going to have to read it directly. As I already said, it’s the MO file equivalent for JS. You can see that the underlying logic is the same. There’s a key-value pair object with original strings and corresponding translations. There’s also our plural-forms header.

One interesting quirk of the make-json command is that, by default, it deletes all the JS translations from the PO files. That’s right, when you run “wp i18n make-json”, your translations disappear from the .po files. If you want to prevent that, you can use the –no-purge argument. In reality, you’d never really edit those files in the terminal to begin with, but we’ll get to that in a second.

After you generate the JSON files and load them with wp_set_script_translations(), your strings should be localized. Here’s the console on the Polish version of the site:

Table of Contents