POT generation script with WP-CLI
A shell script that extracts translatable strings to a .pot file, merges them into .po files and compiles .mo and JSON files.
WordPressInternationalisationi18npotwp-clitranslation
Code
#!/usr/bin/env bash
# Generate and refresh translation files for a theme or plugin.
# Requires WP-CLI with the i18n command package: wp package install wp-cli/i18n-command
set -euo pipefail
TEXT_DOMAIN="${TEXT_DOMAIN:-acme-theme}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LANG_DIR="$ROOT/languages"
POT="$LANG_DIR/$TEXT_DOMAIN.pot"
command -v wp >/dev/null 2>&1 || { echo "wp-cli is required" >&2; exit 1; }
mkdir -p "$LANG_DIR"
echo "==> Extracting strings to $POT"
wp i18n make-pot "$ROOT" "$POT" \
--domain="$TEXT_DOMAIN" \
--exclude="node_modules,vendor,tests,dist,bin" \
--headers='{"Report-Msgid-Bugs-To":"https://example.com/support"}'
echo "==> Checking for untranslated domain mix-ups"
# Any function call that uses a different text domain is almost always a typo.
others=$(grep -rPn --include='*.php' --exclude-dir=vendor --exclude-dir=node_modules \
"(__|_e|esc_html__|esc_attr__|esc_html_e)\\('[^']+', *'(?!${TEXT_DOMAIN}')" "$ROOT" 2>/dev/null | head -n 5 || true)
if [ -n "$others" ]; then
echo "$others"
echo "(warning) other text domains found above" >&2
fi
shopt -s nullglob
for po in "$LANG_DIR"/*.po; do
echo "==> Updating $(basename "$po")"
wp i18n update-po "$POT" "$po"
done
echo "==> Compiling .mo and JSON catalogues"
wp i18n make-mo "$LANG_DIR"
wp i18n make-json "$LANG_DIR" --no-purge
echo "Done. $(grep -c '^msgid ' "$POT") messages in $(basename "$POT")."
How it works
What it solves
Translators need an up-to-date template listing every string, and compiled catalogues WordPress can load. Doing this by hand drifts quickly, so one script regenerates the template, merges changes into each language and builds the binary files.
How it works
The script moves into the project root, checks that WP-CLI exists and runs wp i18n make-pot with the text domain and an exclusion list for build folders. A short check uses a negative lookahead to flag translation calls that use another text domain, a common typo that silently skips translation. It then loops over .po files with update-po, runs make-mo for binaries and make-json for strings used in JavaScript, and prints the message count.
set -euo pipefail stops it on the first failure.
Adapting it
Set TEXT_DOMAIN=acme-core for a plugin. Add --package-name and --file-comment flags for headers. Call the script from a Composer or npm script, or from a CI step that fails if the generated template differs from the committed one. Put language files in a different folder by changing LANG_DIR.
Pitfalls
- The i18n command package must be installed; plain WP-CLI does not include it in every setup.
- Domain checking with grep is a heuristic; dynamic domains need a manual look.
- Do not translate variables; only literal strings are extracted.
- Commit
.potand.po, but build.moin releases if you prefer.
How to use it
Save as bin/make-pot.sh, then run chmod +x bin/make-pot.sh.
Run it from anywhere with ./bin/make-pot.sh; override the domain with TEXT_DOMAIN=acme-core ./bin/make-pot.sh.
Hand the .pot file to translators.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.