DEV3 Technologies home

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 .pot and .po, but build .mo in 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.

Related snippets