wp-cli/i18n-command
Provides WP-CLI commands for WordPress internationalization: generate POT files, build JSON translations, update PO files, and check translation status from the command line. Part of the WP-CLI toolkit for automating i18n workflows.
wp-cli/i18n-command adds a wp i18n namespace to WP-CLI. It is pure PHP — no xgettext, msgfmt, or msgmerge binaries required — so it works identically on macOS, Linux, and Windows CI runners.
Install it once, globally or per-project:
# Global (recommended for local dev)
wp package install wp-cli/i18n-command
# Or pinned per project so CI gets the same version
composer require --dev wp-cli/i18n-command
vendor/bin/wp i18n --help
Look at wp help i18n first, then wp help i18n make-pot. The five subcommands you will actually use are make-pot, make-json, make-mo, make-php, and update-po.
The first use case is almost always generating a POT file for a plugin or theme. Run it from the project root:
wp i18n make-pot . languages/my-plugin.pot
make-pot reads Plugin Name / Theme Name from the main file to infer the slug and text domain, scans PHP (including _n, _x, _nx, _n_noop, esc_html__, etc.) and JavaScript (__, _x, _n, sprintf-style placeholders), and writes a POT with source references. Commit that POT — it is the contract with your translators and with GlotPress/translate.wordpress.org.
Wire the whole chain into a build script, not into manual steps. A typical composer.json script block:
"scripts": {
"i18n": [
"@pot",
"@json"
],
"pot": "wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin --exclude=node_modules,vendor,tests",
"json": "wp i18n make-json languages --no-purge --pretty-print"
}
Generate POT in CI and fail on drift. make-pot exits non-zero when the string audit finds placeholder mismatches or malformed calls, which makes it a useful lint step on its own:
wp i18n make-pot . languages/my-plugin.pot
git diff --exit-code languages/my-plugin.pot
This catches the common case where a developer edits a string and forgets to regenerate the POT.
Use --merge to preserve translator work. When regenerating a POT in a project that keeps translations in-repo, --merge=languages/my-plugin.pot carries over existing translator comments and obsolete entries instead of clobbering them. Combined with wp i18n update-po languages/my-plugin.pot languages/, you get a msgmerge-like workflow fully in PHP:
wp i18n make-pot . /tmp/new.pot --merge=languages/my-plugin.pot
wp i18n update-po /tmp/new.pot languages/
Ship compiled artifacts at release time, not in git. .mo and .l10n.php files are build outputs:
wp i18n make-mo languages/ languages/
wp i18n make-php languages/ languages/ # WP 6.5+ performs better with these
WordPress 6.5+ prefers {domain}-{locale}.l10n.php over .mo, and it loads measurably faster because it is an included array rather than a binary parse. Generate both during a release build so older installs still work.
For block/JS strings, the JSON filename is the integration. Register the script with an explicit path so load_script_textdomain() can find the files:
wp_enqueue_script( 'my-plugin-editor', plugins_url( 'build/editor.js', __FILE__ ), [], '1.0', true );
wp_set_script_translations( 'my-plugin-editor', 'my-plugin', plugin_dir_path( __FILE__ ) . 'languages' );
The md5() hash in my-plugin-de_DE-<hash>.json is derived from the JS file's path relative to the plugin (or theme) directory — this is why the source argument and working directory matter, and why make-json is usually run from the plugin root against the languages folder.
Use the classes directly for custom tooling. The package ships WP_CLI\I18n\PotGenerator, PhpCodeExtractor, and JsCodeExtractor. If you have a non-standard source layout, a custom extractor for a template language, or want POT generation inside a PHPUnit test, calling PotGenerator::generate() from a WP-CLI command of your own is far cleaner than shelling out.
--domain/the inferred slug, it is skipped. Use --ignore-domain when your codebase legitimately mixes domains (e.g. a mu-plugin loading shared code), and use --domain explicitly rather than relying on header inference.make-json silently produces nothing when the PO has no JS references. If the POT was generated with --skip-js, or the .po was created by a tool that dropped #: comments, there is nothing to map. Check that grep -c '\.js:' languages/my-plugin.pot is non-zero.my-plugin-de_DE-abc123.json that never loads almost always means the hash was computed from a different relative path than WordPress computes at runtime. Run make-json from the plugin root and verify by dumping md5( $relative_path ) against the filename. Symlinked plugin directories break this too.--purge / --no-purge defaults have changed between releases. Always check wp help i18n make-json for the version you pinned, and pass the flag explicitly in scripts rather than relying on the default.%s vs %d mismatches, %1$s vs %s inconsistencies between singular and plural, and unbalanced HTML in strings will abort with a non-zero exit. That is the desired behavior in CI; use --skip-audit only for one-off local generations, never in a release pipeline.--file-comment to control the header and accept references as noise, or drop them from the committed POT in a post-processing step. Reviewers should not be reading POT diffs line by line.vendor/ and node_modules/ are excluded by default but not everything is. Custom build directories, .min.js bundles, and copied third-party libraries will be scanned and pollute your POT. Add --exclude= proactively; --debug shows exactly which files were parsed and is the fastest way to diagnose "why is this string missing."--headers replaces --location. Pass a JSON file or inline JSON string for Report-Msgid-Bugs-To, PO-Revision-Date, and friends. The old --location flag is deprecated.wp and vendor/bin/wp. Pick one — usually the project-pinned version in CI — and wp package update the global copy when you hit a bug that is already fixed upstream.How can I help you explore Laravel packages today?