WP-CLI Commands

PerfLocale provides WP-CLI commands for managing languages, translations, strings, caches, and database integrity from the command line.

Namespace: wp perflocale

Languages

List Languages

wp perflocale languages list

Displays all configured languages in a table.

Options:

  • --format=<format> - Output format: table (default), json, csv, yaml, count, ids.

Example:

$ wp perflocale languages list
+----+------+--------+-----------+---------+--------+-----------+
| ID | Slug | Locale | Name | Default | Active | Direction |
+----+------+--------+-----------+---------+--------+-----------+
| 1 | en | en_US | English | No | Yes | ltr |
| 2 | fr | fr_FR | French | Yes | Yes | ltr |
| 3 | de | de_DE | German | No | Yes | ltr |
| 4 | ar | ar | Arabic | No | Yes | rtl |
+----+------+--------+-----------+---------+--------+-----------+

$ wp perflocale languages list --format=json
[{"ID":"1","Slug":"en","Locale":"en_US",...}]

Add Language

wp perflocale languages add <locale> [--default]

Adds a new language.

Arguments:

  • <locale> (required) - WordPress locale code (e.g. fr_FR, es_ES, ar).

Options:

  • --default - Set as the default language.

RTL languages (Arabic, Hebrew, Farsi, Urdu, etc.) are auto-detected.

Examples:

# Add French
$ wp perflocale languages add fr_FR
Success: Language fr_FR added with ID 7 (direction: ltr).

# Add Arabic (RTL auto-detected)
$ wp perflocale languages add ar
Success: Language ar added with ID 8 (direction: rtl).

# Add as default language
$ wp perflocale languages add en_US --default

Delete Language

wp perflocale languages delete <slug>

Deletes a language and unlinks its translations; the posts, terms and media themselves stay. Prompts for confirmation. Cannot delete the default language.

A language that still has content is refused. While any post of the language (a post, page, product or any other post type) is outside the Trash, the command stops before the confirmation prompt with the counts per post type, exits with an error, and changes nothing. Move those posts to the Trash first, or deactivate the language instead, on the Languages screen or with PUT /languages/{slug} and "is_active": false: an inactive language keeps its translations linked. Terms, media and block theme template copies in the language do not block the delete; the command warns that they will be unlinked from their translations.

Arguments:

  • <slug> (required) - Language slug (e.g. fr, de).

Example:

$ wp perflocale languages delete fr
Warning: 8 term(s), 5 media item(s) and 2 block theme template(s) or template part(s) use this language. They will be unlinked from their translations.
Delete language 'fr' (French)? [y/n] y
Success: Language 'fr' deleted.

$ wp perflocale languages delete de
Error: Language 'de' still has content (Posts: 12, Pages: 3, Products: 40). Move those posts to the Trash first, or deactivate the language instead (Languages screen, or REST PATCH is_active=false): an inactive language keeps its translations linked.

Safety features:

  • Refuses a language that still has posts outside the Trash, before the confirmation prompt.
  • Warns about terms, media items and block theme templates that will be unlinked from their translations.
  • Requires explicit confirmation (use --yes to skip in scripts).
  • Blocks deletion of the default language.
  • Reports a delete that did not happen as an error and exits with code 1: “The language could not be deleted; nothing was changed.” when the delete is rolled back, and “Could not check the content of language …; nothing was changed.” when its content cannot be counted.

Translate

Single Post

wp perflocale translate <post_id> --to=<lang> [--provider=<provider>]

Translates a single post using machine translation.

Arguments:

  • <post_id> (required) - The post ID to translate.

Options:

  • --to=<lang> (required) - Target language slug.
  • --provider=<provider> - Provider: deepl, google, microsoft, libretranslate, external_agency, wp_ai_client. Uses configured default if omitted.

Example:

$ wp perflocale translate 42 --to=de
Translating post 42 to de...
Success: Translated! New post ID: 99

A password-protected post is sent when perflocale/mt/send_password_protected allows it (trigger cli_ids, default yes). When the filter refuses it, the command prints “Warning: Post 42: This post is password-protected and was not sent for machine translation.” and translates nothing.

Bulk Translation

wp perflocale translate --all --to=<lang> [--post-type=<type>] [--skip-existing]
wp perflocale translate --post-ids=<ids> --to=<lang> [--skip-existing]

Translates multiple posts with a progress bar.

Options:

  • --all - Translate all published posts of the given type that are in the default language or have no language yet. Translations are never used as sources.
  • --post-type=<type> - Post type for --all. Default: post.
  • --post-ids=<ids> - Comma-separated list of post IDs.
  • --to=<lang> (required) - Target language slug.
  • --skip-existing - Skip posts that already have a translation in the target language.
  • --include-meta - Also machine-translate registered meta fields (SEO titles/descriptions and, when enabled in settings, custom fields).
  • --dry-run - Print a character/cost estimate for the selection (items, characters, monthly budget) without translating anything.
  • --async - With --all: dispatch a resumable chunked background job instead of translating in-process. Track it under PerfLocale → Jobs or wp perflocale jobs list. Without --all the command stops with an error.
  • --provider=<provider> - Translation provider.

Examples:

# Translate all posts to German, skipping already translated
$ wp perflocale translate --all --to=de --post-type=post --skip-existing
Translating to de 100% [============================] 50 / 50
Success: Done. Translated: 38, Skipped: 12, Failed: 0.

# Translate specific posts to French
$ wp perflocale translate --post-ids=42,43,44 --to=fr
Translating to fr 100% [============================] 3 / 3
Success: Done. Translated: 3, Skipped: 0, Failed: 0.

# Translate all WooCommerce products
$ wp perflocale translate --all --to=fr --post-type=product --skip-existing

Notes:

  • Machine translation must be enabled - turn the feature on at PerfLocale → Addons, then configure it at Settings → Addons → Machine Translation.
  • Failed translations are reported individually but don't stop the batch.
  • A password-protected post is sent when perflocale/mt/send_password_protected allows it. With --post-ids the trigger is cli_ids, which sends by default; with --all (also with --async) it is cli_all, which does not. A post the filter refuses counts as Skipped.
  • Progress bar shows real-time progress.
  • A machine translation whose block structure does not match its source is not saved and counts as Failed, with the reason in the warning for that post (see Machine Translation).
  • When the target language is deleted or deactivated while the command runs, the posts not yet done count as Failed. After a delete, a translation the command created for that language in its current batch of up to 500 posts is deleted again and counted as Failed; translations from earlier batches stay, and after a deactivation every translation already written stays. The warnings for these posts follow the progress bar. Renaming the language does not stop the run.

Find Damaged Block Markup

wp perflocale damaged-blocks [--lang=<slug>] [--post-type=<type>] [--batch-size=<number>] [--format=<format>]

Lists translations whose content has damaged block markup: a block comment saved as visible text (&lt;!-- wp:), block-comment text that is not a block comment (a cut-off opener), blocks opened and not closed, or a block that lost settings its source block has. A translation is listed only when it has more of a kind than its default-language source, so an article about block markup is not listed. Every translation is read, whatever its status, the Trash included. Changes no content or settings; reads in batches. See Machine Translation for how to repair a listed translation: translate its source_id again.

Options:

  • --lang=<slug> - Only translations in this language. The default language has no translations, so it lists nothing.
  • --post-type=<type> - Only translations of this post type.
  • --batch-size=<number> - Translations read per query. Default: 200; a value outside 1 to 1000 is set to the nearest limit.
  • --format=<format> - table (default), csv or json. An empty JSON result is [].

Columns: post_id (the translation), language, source_id (its default-language source), issue (escaped block delimiter, block delimiter left as text, unbalanced block delimiters or lost block attributes) and spot (the byte offset of the first damaged spot and the next 60 bytes; for lost settings, their names first).

Examples:

# Every damaged translation
$ wp perflocale damaged-blocks
+---------+----------+-----------+-------------------------+-------------------------------------------------+
| post_id | language | source_id | issue                   | spot                                            |
+---------+----------+-----------+-------------------------+-------------------------------------------------+
| 812     | de       | 640       | escaped block delimiter | byte 1043: &lt;!-- wp:group {&quot;layout&quot;... |
| 815     | de       | 655       | lost block attributes   | lost layout at byte 81: <!-- wp:group -->...     |
+---------+----------+-----------+-------------------------+-------------------------------------------------+

# Translate both sources again into German (source_id column)
$ wp perflocale translate --post-ids=640,655 --to=de

# German pages as JSON, for a script (exit status 2 when something is listed)
$ wp perflocale damaged-blocks --lang=de --post-type=page --format=json

# Nothing found
$ wp perflocale damaged-blocks
Success: No translation has damaged block markup.

Exit status: 0 when no translation is listed, 2 when translations are listed (after the list), 1 when the command cannot run, for example an unknown --format (“Unknown --format. Use table, csv or json.”), an unknown language (“Unknown language: xx”), or a database read that failed (“The scan stopped because a database read failed. Run the command again.”). A run that fails prints no rows.

Translations

List Translations

wp perflocale translations list <post_id> [--format=<format>]

Shows all translations linked to a post.

Example:

$ wp perflocale translations list 42
+----------+---------+--------------------+---------+
| Language | Post ID | Title | Status |
+----------+---------+--------------------+---------+
| EN * | 42 | Hello World | publish |
| FR | 160 | Bonjour le monde | publish |
| DE | 165 | Hallo Welt | publish |
+----------+---------+--------------------+---------+

The * marks the current post's language.

Create Translation

wp perflocale translations create <post_id> --to=<lang>

Creates a new translation post (copies content from source).

Example:

$ wp perflocale translations create 42 --to=fr
Success: Translation created. New post ID: 200

Delete Translation

wp perflocale translations delete <post_id> --lang=<slug>

Permanently deletes a translation post. Prompts for confirmation.

Example:

$ wp perflocale translations delete 42 --lang=fr
Are you sure you want to proceed? Delete fr translation (post 200)? [y/n] y
Success: Translation deleted (post 200).

Set Language

wp perflocale translations set-language <post_id> --lang=<slug>

Changes the language of an existing post. Fails if the language is already used by a sibling in the same translation group. When the language actually changes, the post’s terms in translatable taxonomies are swapped for their translations in the new language. A term with no translation in that language, or with no language of its own, is kept as it is.

Example:

$ wp perflocale translations set-language 42 --lang=en
Success: Post 42 language set to en.

$ wp perflocale translations set-language 42 --lang=fr
Error: Failed to set language. The language may already be used by a sibling translation.

Point Translated Templates at Their Menu and Pattern Translations

wp perflocale template-refs-backfill [--batch-size=<number>]

Runs the one-time update job again, in the WP-CLI process: every template and template part translated into a language other than the default one, whose Navigation block or synced pattern still names the original-language menu or pattern that its original names too, is pointed at that menu’s or pattern’s translation in its language, when the translation exists and the Site Editor can open it (a published or draft menu; a published, draft or pending pattern). A menu or pattern chosen in the Site Editor is kept, also a deliberate switch back to the original. What a visitor sees does not change: the page already shows the translation. The run is recorded as done for the site. See Translating menus in block themes.

Options:

  • --batch-size=<number> - Templates and template parts read per query. Default: 50; a value outside 1 to 500 is set to the nearest limit.

Example:

$ wp perflocale template-refs-backfill
Success: 24 template(s) and template part(s) read, 3 updated, 4 reference(s) pointed at a translation.

Exit status: 0 when it finished; 1 when the templates and template parts could not be read (a database error, or no default language), or when one could not be saved, for example because it was edited at the same time. The error names the template or part and the counts so far, and the run is then not recorded as done; run the command again.

Strings

Scan for Strings

wp perflocale strings scan [--domain=<domain>] [--dir=<dir>]

Scans PHP files for translatable strings (__(), _e(), _x(), _n(), _nx(), _n_noop()).

Options:

  • --domain=<domain> - Only scan strings for a specific text domain.
  • --dir=<dir> - Directory to scan. Default: active theme directory. (Named --dir because --path is wp-cli’s own global WordPress-root flag and never reaches the command.)

Examples:

# Scan active theme
$ wp perflocale strings scan
Scanning /var/www/html/wp-content/themes/flavor/flavor...
Success: Found 342 strings, inserted 15 new.

# Scan a specific plugin
$ wp perflocale strings scan --dir=/var/www/html/wp-content/plugins/woocommerce --domain=woocommerce
Scanning /var/www/html/wp-content/plugins/woocommerce...
Success: Found 4821 strings, inserted 4821 new.

Performance:

  • Batched inserts (500 per batch) - handles large codebases.
  • Skips files over 2 MB to prevent memory exhaustion.
  • Excludes vendor/, node_modules/, .git/, tests/, build/, dist/.

Slugs

Backfill Slug Translations

wp perflocale slugs backfill

Populates the slug translation table for all existing term translation groups. This ensures that translated taxonomy URLs work correctly.

When to run:

  • After migrating from another multilingual plugin.
  • After creating term translations manually.
  • If taxonomy archive URLs show 404 errors.

Example:

$ wp perflocale slugs backfill
Success: Backfilled 48 slug translation entries.

Verify Slug Translations

wp perflocale slugs verify

Checks if all terms in translation groups have corresponding slug translation entries.

Example:

$ wp perflocale slugs verify
Success: All term translations have slug entries.

$ wp perflocale slugs verify
Warning: 12 terms are missing slug translations. Run 'wp perflocale slugs backfill' to fix.

Cache

Flush Caches

wp perflocale cache flush

Flushes PerfLocale’s caches in all three cache layers, and only those:

  • L1: In-memory static cache.
  • L2: PerfLocale’s entries in the WordPress object cache (Redis/Memcached if available). The rest of a persistent object cache, other plugins’ data and WordPress’s own, is kept.
  • L3: PerfLocale’s transients (wp_options table).

State that is not a cache is kept: notices still waiting to be shown to a user, circuit breakers and rate-limit windows. The Clear Cache button in the Cache panel of PerfLocale → Dashboard does the same.

Example:

$ wp perflocale cache flush
Success: All PerfLocale caches flushed.

Migrate

Migrate from another multilingual plugin

wp perflocale migrate <source> [--dry-run] [--yes] [--force-restart]

Imports translation data from another multilingual plugin into PerfLocale’s own tables. The source plugin’s data is not modified or deleted - the importer is read-only on the source side and writes new rows on the PerfLocale side.

Arguments:

  • <source> (required) - one of wpml, polylang, translatepress.

Options:

  • --dry-run - confirm the source is detected and importable without writing anything. When the import would run despite problems, for example content in a language with no PerfLocale language, the dry run ends with a warning naming what it would skip instead of Success:, for every source. Problems that would refuse the import end with an error (exit status 1).
  • --yes - skip the interactive “Are you sure?” prompt (use for scripting / CI).
  • --force-restart - clear the migration source-map for this importer before the run. Use only after a deliberate DB restore to a known-good pre-migration state when you want fresh translation_groups allocated rather than reusing mappings from the prior run. Running this on a database whose translation_groups already point at the source plugin’s data WILL orphan those groups. The default (omit) is the safer choice; see the migration disaster-recovery section for details.

Examples:

# Confirm WPML data is detected.
$ wp perflocale migrate wpml --dry-run
Dry run - checking wpml data...
Success: Wpml data detected and ready to import. Run without --dry-run to proceed.

# Run the real import, prompt for confirmation.
$ wp perflocale migrate wpml
Migrate translation data from wpml? This cannot be undone. [y/n] y
Migrating from wpml...
Success: Migration complete. Imported: 234 posts, 89 terms, 1412 strings.

# Scripted (no prompt).
$ wp perflocale migrate polylang --yes
Migrating from polylang...
Success: Migration complete. Imported: 128 posts, 34 terms.

# A Polylang import that left items out ends with a warning, not Success (exit status 0).
$ wp perflocale migrate polylang --yes
Migrating from polylang...
Warning: 2 Polylang items were not imported: their posts or terms no longer exist. For example: post 24 (fi), term 49 (sv).
Warning: Migration finished. Imported: 49 posts, 21 terms. Some Polylang items were not imported and need attention (see above).

# Operator workflow: restored a clean pre-migration backup, want a fresh
# allocation rather than reusing the prior run’s source-map mappings.
$ wp perflocale migrate wpml --force-restart --yes
--force-restart: cleared 234 source-map row(s) for wpml.
Migrating from wpml...
Success: Migration complete. Imported: 234 posts, 89 terms, 1412 strings.

In scripts, check the exit status instead of looking for Success:: an import that finished but left items out ends with Warning: and exit status 0; one that was refused or did not finish ends with Error: and exit status 1. A Polylang import that could not read or write part of the data ends with “The import did not finish: some data could not be read or written (see the warnings above). What it imported is kept. Run it again to import the rest.”

Per-source guides: WPML • Polylang • TranslatePress. For the shared pre-flight steps and safety model, see the migration overview.

Memory tuning on very large sites: all three importers chunk their source-table SELECTs. The default is fine up to tens of thousands of posts (32 MB peak in our 5,000 group × 3 language benchmark). On tighter memory_limit hosts you can shrink the chunk size with perflocale/migration/wpml/batch_size, perflocale/migration/polylang/batch_size, or perflocale/migration/translatepress/batch_size filters.

WooCommerce

Find Language Copies Whose Stock Differs

wp perflocale stock-drift [--product=<ids>] [--fix] [--from=<from>] [--yes] [--batch-size=<number>] [--format=<format>]

Inventory Sync keeps the language copies of a product as one stock. This reads every product translation group and lists the groups whose copies hold different stock quantities: products that manage their own stock, and each published or private variation with its matching variation in every other language. Managed stock that allows backorders is compared too. Copies that do not manage their stock, copies excluded from the sync and products in the Trash are left out. Without --fix it changes no stock, content or settings; reads in batches. On multisite it checks the site --url names. Needs WooCommerce, the WooCommerce add-on and its stock sync setting.

Options:

  • --product=<ids> - Only these products: IDs of any language copy, comma-separated, up to 500 (not variations).
  • --fix - Not available in this version: the command answers “Aligning stock is not available in this version.” and exits with status 1.
  • --from=<from> - Used with --fix only.
  • --yes - Used with --fix only.
  • --batch-size=<number> - Translation groups read per query, 1 to 500. Default: 200.
  • --format=<format> - table (default), csv or json. An empty JSON result is [].

Columns: one row per copy in a group that differs, the copy the others are compared with included: group (that copy: the default-language product or variation, or the lowest ID when no copy is in the default language), product_id, parent_id (the variable product of a variation, 0 for a product), language and stock.

Examples:

# Every copy group whose stock differs
$ wp perflocale stock-drift
+-------+------------+-----------+----------+-------+
| group | product_id | parent_id | language | stock |
+-------+------------+-----------+----------+-------+
| 210   | 210        | 0         | en       | 12    |
| 210   | 344        | 0         | es       | 2     |
| 210   | 351        | 0         | de       | 12    |
+-------+------------+-----------+----------+-------+
Warning: 1 copy group has stock that differs.

# As JSON, for a script (exit status 2 when something differs)
$ wp perflocale stock-drift --format=json

# Nothing differs
$ wp perflocale stock-drift
Success: No copy group has stock that differs.

Exit status: 0 when no copies differ, 2 when copies differ (they are listed first), 1 when the command cannot run: “Inventory Sync is not active on this site (WooCommerce, the WooCommerce add-on and its stock sync setting).”, “--batch-size must be between 1 and 500.”, an unknown --format, or a database read that failed. A run that fails prints no rows.

Database Migrations

Run pending schema migrations

wp perflocale db-migrate

Runs every pending PerfLocale database upgrade step at once, without waiting for the next automatic try, and checks that every table, column and index is in the database. Idempotent - safe to run multiple times. Useful in CI/CD pipelines or headless environments: without it, the upgrade runs on the first admin, REST API or WP-CLI request after an update, or in the background worker (see The database upgrade did not finish).

A step that fails is printed as a warning with the database’s error, and the command exits non-zero with “The PerfLocale upgrade did not finish. Fix the cause above, then run this command again.” While another request is running the upgrade, it stops with “Another request is running the PerfLocale upgrade. Run this command again in a few minutes.”

Example:

$ wp perflocale db-migrate
Success: PerfLocale database migrations completed successfully.

On SQLite (WordPress Playground, and hosts that use the SQLite Database Integration), the command also runs the one-time repair of string translation groups to the end, which otherwise runs as a background job in small steps. It prints its progress, and the success line then says what it changed (“SQLite string groups repaired: …”). While the background job is working on the repair, the command stops with an error that says to run it again later.

Health Check

Run Database Integrity Checks

wp perflocale health-check [--fix]

Runs ten database integrity checks. Without --fix it only reports and writes nothing.

Options:

  • --fix - Automatically fix found issues.

Checks performed, in this order:

  1. Post or term links replaced by a string import - imported links of a post or term group that name the ID of a string instead. Each is listed (the first 20) with its group, language, the ID it names, the reason and the group’s other members. --fix removes the ones that name no post or term (counted again under orphaned links); the others name an existing post or term, so --fix leaves them for you to check by hand.
  2. Strings without their own translation group - strings whose group is missing, is not a string group, or is shared with another string, with the first 20 string IDs. --fix gives each one its own group and moves its links there, one string at a time; a string whose change fails is rolled back and reported. Translation files are then compiled again when strings are served from files, and perflocale/strings/changed fires with the origin health_check.
  3. Orphaned post links - Translation links pointing to deleted posts.
  4. Orphaned term links - Translation links pointing to deleted terms.
  5. Invalid language references - Links referencing deleted languages.
  6. Widow translation groups - Non-string groups with zero remaining links. String-type groups are preserved (they are valid with no links until a translation is saved).
  7. Orphan string groups - String-type groups whose owning row in the strings table is gone.
  8. Groupless orphan links - Translation links whose parent group row no longer exists.
  9. String links without a translation - a string’s translation link in a language with no translation text, left when a save or a machine translation run stopped between its two writes. Reported only: --fix leaves them; type the translation on the Strings screen, or run machine translation with skip existing.
  10. Strings whose text disagrees with its hash - --fix repairs a string whose text was only entity-encoded. Text that was stripped cannot be rebuilt; the next string scan adds it again from the source.

The --fix flag removes the offending rows directly and then flushes all PerfLocale caches, so stale cache entries can't resurrect the deleted rows. On SQLite, while the one-time repair of string translation groups (see Database Migrations) has not finished, --fix leaves check 2 to it: “Not fixed: the SQLite string-group repair has not finished. Run wp perflocale db-migrate, then this check again.” When every check passes, the command ends with “All ten orphan, duplicate and string-integrity checks passed.”

Examples:

# Report only
$ wp perflocale health-check
Running PerfLocale health checks...

Warning:  Post or term links replaced by a string import: 1
   Post group #812, de: the link names #57 (no such post); the group's other members: #640.
   --fix removes the ones that name no post or term (counted under orphaned links below).
 Strings without their own translation group: 0 ✓
Warning:  Orphaned post links: 1
 Orphaned term links: 0 ✓
 Invalid language references: 0 ✓
Warning:  Widow translation groups (non-string): 2
 Orphan string groups: 0 ✓
 Orphan links (missing group): 0 ✓
 String links without a translation: 0 ✓

 Strings matching their hash: all ✓
Warning: Found 3 issues. Run with --fix to repair.

# Auto-fix
$ wp perflocale health-check --fix
Running PerfLocale health checks...

 Post or term links replaced by a string import: 0 ✓
Warning:  Strings without their own translation group: 2 (group missing: 2, not a string group: 0, shared: 0)
   String ids: 101, 102.
 Fixed: gave 2 string(s) their own translation group (0 link(s) moved, 1 link(s) added).
 Orphaned post links: 0 ✓
 Orphaned term links: 0 ✓
 Invalid language references: 0 ✓
Warning:  Widow translation groups (non-string): 2
 Fixed: removed 2 widow groups.
 Orphan string groups: 0 ✓
 Orphan links (missing group): 0 ✓
 String links without a translation: 0 ✓

 Strings matching their hash: all ✓
Success: Fixed 4 issues.

Status

Translation Completeness Overview

wp perflocale status [--format=<format>]

Displays translation completeness per language.

Options:

  • --format=<format> - Output format: table (default), csv, json, yaml, count.

Example:

$ wp perflocale status
+--------------+-----------+------------+-------+---------+--------------+-------+-------+
| Language     | Published | Translated | Draft | Pending | Needs Update | Empty | Total |
+--------------+-----------+------------+-------+---------+--------------+-------+-------+
| English (en) | 12        | 0          | 2     | 0       | 0            | 0     | 14    |
| French (fr)  | 14        | 0          | 0     | 0       | 0            | 0     | 14    |
| German (de)  | 8         | 0          | 1     | 0       | 0            | 5     | 14    |
| Arabic (ar)  | 3         | 0          | 0     | 0       | 0            | 11    | 14    |
+--------------+-----------+------------+-------+---------+--------------+-------+-------+

$ wp perflocale status --format=json
[{"Language":"English (en)","Published":12,...}]

Export & Import

Export a single site

wp perflocale export <file>

Writes a single JSON envelope with the current site’s PerfLocale settings and translation data - every section of the export screen: settings, roles & capabilities, languages, translation links, string translations, slug translations, content hashes, media, menus, item flags and add-on data (see Export & Import). The envelope embeds a format_version field so importers refuse payloads written by a newer format. Settings whose names mark them as credentials (API keys, tokens, secrets, passwords and the like, in the main settings and in each add-on’s settings) are left out, so a backup never carries API keys.

$ wp perflocale export /tmp/perflocale-backup.json
Success: Exported to /tmp/perflocale-backup.json (1433.6 KB).

Import into a single site

wp perflocale import <file> [--mode=<mode>] [--yes] [--force] [--addon-data=<choices>] [--batch-size=<rows>]

Options:

  • --mode=merge (default) - additive; existing rows are kept, new rows are inserted. A row that points at a parent the same bundle carries is re-pointed at the row that parent actually landed on. Post and term IDs are never re-mapped - see what travels between sites. Merge never changes this site’s configuration: when the file carries settings, add-on settings, the add-on list or roles, they are counted as skipped and the command prints Warning: Settings, add-on settings, the add-on list and roles in the file were not applied: merge mode keeps this site's configuration. Run the import with --mode=replace to apply them (a file that carries only those sections deletes no rows).
  • --mode=replace - clears this site’s rows for every section the file declares (scoped to the current site on multisite) and then writes the envelope verbatim, applying the file’s settings, add-on settings, add-on list and roles. This site’s own domains, provider addresses, uninstall choice and paused queue are kept, and a warning names any whose value in the file differed (see Merge vs. replace). A file that carries only the Settings and Roles & Capabilities sections owns no table, so it deletes no rows. Asks “Replace mode deletes this site's PerfLocale rows for every section the file declares, then loads the file. Continue?” first.
  • --yes - skip the confirmation prompt of --mode=replace.
  • --force - import a bundle that is refused because its recorded site address is not this site’s (see Import refused). Use it only when this site is a copy of the exporting one and the post and term IDs still match (a staging clone, a domain migration, or this site’s own backup that recorded a different address). A file that records no address is not checked; one whose recorded address cannot be read counts as a mismatch. With --mode=replace a foreign bundle is refused when it declares any section other than Settings or Roles & Capabilities, carrying rows for it or not, because replace deletes this site’s rows for those sections first. The command exits non-zero on a refusal. Same meaning as network-import --force.
  • --addon-data=<choices> - the add-on data to import, as the Add-on data to import checkboxes on the import screen choose it: a comma-separated list of addon_data (the Add-on data checkbox) and the keys of the add-ons’ own checkboxes (<owner>/<group>, as the file’s addon_groups names them), or none. Default: all the add-on data the file carries. Add-on data whose add-on is not active on this site is skipped and named in a warning. Any other value stops the command with “Import failed: Unknown --addon-data value: …”.
  • --batch-size=<rows> - rows the importer reads and checks per piece, a whole number from 50 to 5000, for this run, in place of the perflocale/import/batch_size filter (default 500). Larger pieces need fewer look-up queries; smaller ones keep peak memory lower. Any other value stops the command with “Invalid --batch-size: use a whole number from 50 to 5000.”
# Restore from backup, additive
wp perflocale import /tmp/perflocale-backup.json

# Replace this site’s rows for every section the envelope declares
wp perflocale import /tmp/perflocale-backup.json --mode=replace --yes

# This site is a copy of the site the file came from
wp perflocale import /tmp/staging.json --force

# Only the add-on data of the shared Add-on data checkbox
wp perflocale import /tmp/perflocale-backup.json --addon-data=addon_data

# Larger pieces on a site with plenty of memory
wp perflocale import /tmp/perflocale-backup.json --batch-size=2000

Output. What the import left out, kept or changed on purpose is printed as one Warning: line per note, for example the settings Replace kept or the rows it skipped with their reason. The import ends with Success: Import complete. Imported: N, Skipped: N., or with Error: Import finished with N error(s). Imported: N, Skipped: N. after one warning per error. Only one data import runs on a site at a time: while another one runs, the import is refused with “Import refused: another import is running on this site. Wait for it to finish (PerfLocale → Jobs lists background imports), then import the file again.”

Exit code. import, po-import and network-import exit non-zero when the import reports errors, so a restore script can rely on the exit code alone - a malformed file ends in an error, not Success:. Ordinary duplicate-key merge skips are not errors and do not affect the exit code.

Replace is all-or-nothing. The wipe and the reload run inside one transaction: any row the database refuses rolls the whole import back. The settings, add-on settings, disabled-add-on list and role grants the file carries are applied only after the tables are committed, so a failed import changes none of them. You keep your pre-import state and get an error naming the table, and the row when it can be named.

Stored exactly as the file has it. The command restores string translations, languages and translated slugs byte for byte, whichever user it runs as. That differs from the import form on Settings → Export & Import, which cleans those values for an account without the unfiltered_html capability (Export & Import).

Export string translations as PO

wp perflocale po-export <file> --lang=<slug> [--domain=<domain>]

Writes a gettext PO file for one language. Optional --domain filters to a single text domain (default: all domains).

Each plural string is written as one gettext plural entry with every form the language has, so po-import of the file restores all forms. When the export cannot pair a plural text with its singular, or the language has fewer plural forms than a string stores, the command warns before its success line: “2 plural string(s) have plural forms that are not in this file: their singular string could not be identified, or the language has fewer plural forms. A --replace import of this file removes those forms; a merge import keeps them. First ids: 418, 422”.

$ wp perflocale po-export /tmp/fr.po --lang=fr
Success: PO exported to /tmp/fr.po (58.3 KB).

$ wp perflocale po-export /tmp/fr-myplugin.po --lang=fr --domain=my-plugin

Import string translations from PO

wp perflocale po-import <file> --lang=<slug> [--replace]

Options:

  • --replace - wipes every existing translation for this language first (in one transaction), then imports the file; msgids absent from the PO file end up untranslated. Without it, the import is an upsert: new msgids are inserted and existing msgids are updated with the file's translation.

Robust to \r\n line endings, and skips fuzzy and obsolete entries. A file with nothing it can import - every translation marked fuzzy, or only extra plural forms (msgstr[2] and up) - is refused with an error, the command exits non-zero, and nothing is changed. Translations are stored byte for byte, whichever user the command runs as; the Import PO button on PerfLocale → Strings is the path that filters HTML for an account without unfiltered_html (String Translation).

wp perflocale po-import /tmp/fr.po --lang=fr
wp perflocale po-import /tmp/fr.po --lang=fr --replace

Export a multisite network

wp perflocale network-export <file> [--include-inactive]

Multisite only. Walks every site in the network and writes a single envelope keyed by blog ID, marked network: true. Inactive sites are skipped unless --include-inactive is passed.

wp perflocale network-export /tmp/network.json
wp perflocale network-export /tmp/network.json --include-inactive

Import a multisite network

wp perflocale network-import <file> [--mode=<mode>] [--yes] [--site=<id>] [--force] [--batch-size=<rows>]

Options:

  • --mode=merge (default) or --mode=replace - same semantics as single-site import, applied per site. In merge mode the warning about settings, add-on settings, the add-on list and roles is printed per site, prefixed with the source site’s ID. Every note and error of the import is prefixed the same way (Warning: Site <id>: …).
  • --yes - skip the confirmation prompt of --mode=replace (“Replace mode will WIPE matching data on every targeted site. Continue?”), for scripted runs.
  • --site=<id> - pick one source site_id slice; the current blog receives the data. Required when loading a network envelope outside multisite.
  • --force - import a slice even when its recorded site_url doesn't match the target blog (by default mismatched slices are skipped with a warning). Use after a domain migration when the blog IDs are known-correct. Addresses compare as host, port and path with the scheme ignored, against the target blog’s home option, with home_url() accepted as well: an http-to-https move still matches, two installs that differ only by port do not.
  • --batch-size=<rows> - rows the importer reads and checks per piece, from 50 to 5000, for this run; as for import.

The file is read in pieces, slice by slice, so its size does not decide the memory the import needs. The limits of perflocale/import/quality_limits apply as they do to import.

# Across the whole network (multisite), without the prompt
wp perflocale network-import /tmp/network.json --mode=replace --yes

# Restore one slice on a non-multisite target
wp perflocale network-import /tmp/network.json --site=42

Addons

Inspection-focused subcommands for the Addon System. list, info, orphans, doctor, and errors (without --clear) are read-only; migrate, reset-version, enable, disable, and reset-quarantine write. There is no uninstall subcommand by design.

List Addons

wp perflocale addon list [--format=<format>] [--fields=<fields>] [--all]

Options:

  • --format=table|json|csv|yaml|ids|count (default: table).
  • --fields=<comma-separated> - pick specific columns. Available: id, name, bundled, booted, disabled, compatible, has_schema, schema_target, schema_stored, has_uninstall, has_custom, manifest. Default: all.
  • --all - include orphan manifests (addons whose class is gone but whose manifest is still in the database).

The default table view exposes disabled and booted alongside bundled so you can see lifecycle state in one place without running doctor separately.

wp perflocale addon list
wp perflocale addon list --format=json --all
wp perflocale addon list --fields=id,disabled --format=csv
wp perflocale addon list --format=ids        # plain id list for shell loops

Inspect a Single Addon

wp perflocale addon info <id> [--format=<format>]

Shows the addon's full manifest plus a live PurgePlan preview (counts only - no DB mutation). Use this to verify that a custom addon's tables, options, and capabilities are namespaced correctly before relying on the uninstall contract.

wp perflocale addon info my-addon
wp perflocale addon info my-addon --format=json

List Orphans

wp perflocale addon orphans

Lists addon IDs that still have a manifest row but whose registering class is no longer loaded - typically because the addon's plugin was deactivated or deleted before PerfLocale itself was uninstalled.

Run Addon Migrations

wp perflocale addon migrate [<id>] [--force]

Options:

  • <id> - optional; migrate just this addon (default: all).
  • --force - reset the stored schema version to 0 and re-run every migration step. Dangerous.
wp perflocale addon migrate
wp perflocale addon migrate my-addon
wp perflocale addon migrate my-addon --force

Migration / Uninstall Errors

wp perflocale addon errors [<id>] [--clear]

Tail the addon migration / uninstall error log. Pass an addon ID to filter, or --clear to empty the log (optionally for one addon).

wp perflocale addon errors
wp perflocale addon errors my-addon
wp perflocale addon errors --clear
wp perflocale addon errors my-addon --clear

Reset Stored Schema Version

wp perflocale addon reset-version <id> <version> [--yes]

Manual recovery only - sets the stored schema version for one addon to a specific value. Setting it lower than the addon's current target causes the next migration pass to re-run every step in between. Confirms before writing unless --yes is passed.

wp perflocale addon reset-version my-addon 0
wp perflocale addon reset-version my-addon 2 --yes

Health Summary

wp perflocale addon doctor [--format=<format>]

One-shot operator summary: counts of booted / disabled / quarantined / version-mismatched addons in a single table with the affected IDs. The first command to run when an addon isn't behaving as expected.

wp perflocale addon doctor
wp perflocale addon doctor --format=json

Enable / Disable

wp perflocale addon enable <id>
wp perflocale addon disable <id>

Mirror of the per-card Enable / Disable button on the Addons admin page. Writes the addon ID into the perflocale_disabled_addons option and flushes the bootable-addons transient so the change applies on the very next request. Works for both bundled (in-plugin) and external addons - disabling a bundled addon takes effect immediately, so only disable one if you genuinely don't want that integration.

wp perflocale addon disable my-addon
wp perflocale addon disable woocommerce
wp perflocale addon enable my-addon

Read / Write Addon Settings

wp perflocale addon settings get <addon-id> <key> [--default=<val>] [--format=scalar|json]
wp perflocale addon settings set <addon-id> <key> <value> [--type=string|bool|int|float|json]
wp perflocale addon settings list <addon-id> [--format=table|json|csv|yaml]

Get, set, and list values for an addon’s settings without going through the admin UI. settings set coerces the value to the supplied --type (default string) and returns an error if the addon’s entry would exceed the 16 KiB per-addon cap.

'storage' => 'global' fields. Some addons (the bundled WooCommerce addon is the canonical example) keep their settings in the main perflocale_settings option instead of in the addon-storage option, because the runtime + REST APIs read from there. For those fields the CLI does the right thing automatically:

  • settings get transparently reads from perflocale_settings, so the value you see is the value the runtime actually uses.
  • settings set writes through Settings::update() rather than into addon storage, so the full invalidation chain runs (config-cache flush, cron reschedules, settings-updated listeners). Prefer it over wp option patch update perflocale_settings <key> <value>, which changes the stored value but leaves all of that stale. If a sanitizer coerces the value - or an environment variable / PHP constant overrides it - the write still happens and the command warns you with the effective value.
  • A settings set of a global-storage field that is not saved stops with an error that names the key and the cause: “<key> not saved: another settings save was running. Run the command again.”, “<key> not saved: the database refused to store the option perflocale_settings, so the value may be too long or hold characters it cannot store. Check the value, then run the command again.” (with a line on where the database error is) or “<key> not saved: a filter on the option perflocale_settings kept the stored value.” A new mt_agency_url must start with https://, or be an http:// address on a host added to the perflocale/mt/trusted_hosts filter (External agency).
  • An add-on-storage settings set that is not saved says why: “Could not save '<addon>.<key>': another add-on settings save was running. Run the command again.”, that the add-on’s settings would be larger than the size stored per add-on, that the database refused the option perflocale_addon_settings, or that a filter on it kept the stored value.
  • settings list shows addon-storage AND global-storage fields together with a storage column on every row, so you can see exactly where each setting lives.
# Addon-storage field (lives in perflocale_addon_settings):
wp perflocale addon settings get my-addon batch_size
wp perflocale addon settings set my-addon batch_size 50 --type=int
wp perflocale addon settings set my-addon limits '{"max":100}' --type=json

# Global-storage field (lives in perflocale_settings):
wp perflocale addon settings get woocommerce wc_email_translation     # reads live value
wp perflocale addon settings set woocommerce wc_email_translation false --type=bool
# → Success: Saved wc_email_translation to the global perflocale_settings option.

# settings list shows both kinds, with a `storage` column:
wp perflocale addon settings list woocommerce
# key                          value         storage
# wc_email_translation         true          global
# wc_sync_stock                true          global
# ...

Reset Failure Quarantine

wp perflocale addon reset-quarantine <id>

Clear an addon's failure counter after the underlying boot error has been fixed. Quarantined addons (3 consecutive boot failures) are otherwise skipped until manually cleared - this is the manual-clear path.

wp perflocale addon reset-quarantine my-addon

Background Jobs

Inspect and control the background-processing queue from the command line. See the Background Jobs doc for the full feature reference.

Namespace: wp perflocale jobs

List Jobs

wp perflocale jobs list [--status=<status>] [--format=<format>]

Lists all jobs in the active-jobs index.

Options:

  • --status=<status> - Filter by status: queued, running, complete, failed, canceled.
  • --format=<format> - Output format: table (default), json, csv, yaml.

Example:

$ wp perflocale jobs list
+--------------------------------------+--------------+----------+----------+--------------------+
| id                                   | type         | status   | progress | updated_at         |
+--------------------------------------+--------------+----------+----------+--------------------+
| 5fc94964-29f6-4697-8c4e-64a8ffb5c72d | string_scan  | running  | 47%      | 12 seconds ago     |
| 9d291e3c-3a41-47ab-b3e6-3ba00e88e55a | data_import  | queued   | 0%       | 8 seconds ago      |
+--------------------------------------+--------------+----------+----------+--------------------+

$ wp perflocale jobs list --status=failed --format=json
[{"id":"...","type":"bulk_translate","status":"failed",...}]

Get Job Details

wp perflocale jobs get <id> [--format=<format>]

Prints the full state of one job: type, hook, engine, status, progress, args, log ring buffer, error message, result payload, attempts, created_at / completed_at / updated_at, created_by user id.

Arguments:

  • <id> (required) - Job UUID (visible in jobs list).

Options:

  • --format=<format> - Default yaml (one key:value per line); json for machine-readable.

Cancel a Job

wp perflocale jobs cancel <id>

Cancels a queued or running job. Sets status to canceled and unschedules the worker event. A worker that's currently mid-execute() aborts cooperatively on its next progress callback - the delay until that happens depends on the job's batch size (most jobs tick every batch, so the lag is usually brief, but a large batch can hold it open for a few seconds). Very short jobs that complete before any progress tick may still finish naturally.

Refuses jobs that are already complete / failed / canceled with a warning. Canceling an XLIFF import also deletes its stored file.

Retry a Job

wp perflocale jobs retry <id>

Re-enqueues a failed or canceled job. Resets status to queued, clears the error and result fields, schedules a new worker event with the original args + hook.

Refuses non-terminal jobs (queued / running / complete) with a warning.

Run a Job in WP-CLI

wp perflocale jobs run <id>

Runs one job in this WP-CLI process until it ends, without a web request’s time limit: a job that runs in slices, such as a bulk translation or an XLIFF import, runs each slice here, with no time or item limit by default. A failed or canceled job is retried first. Each slice and each retry starts at the time the worker planned for it, for example after a wait a translation provider asked for, and a line such as Job 5fc94964-…: 120 of 500 done; next run in 0 s. is printed between them. The job runs with its owner’s rights, as in any worker.

While the command runs, WP-Cron and Action Scheduler leave the job to it, so DISABLE_WP_CRON is not needed. Should the process stop, the job goes back to them within 5 minutes. The command ends with success when the job completes, and with an error when it fails, is canceled, or is taken over by another worker. It refuses a job that is already complete, a job another wp perflocale jobs run holds (“Wait for it to end; its hold lapses 300 seconds after it stops.”), and a paused queue (“The job queue is paused. Unpause it first: wp perflocale jobs unpause”). When the queue is paused while it runs, the command stops with an error and the job keeps its place.

$ wp perflocale jobs run 5fc94964-29f6-4697-8c4e-64a8ffb5c72d
Success: Job 5fc94964-29f6-4697-8c4e-64a8ffb5c72d complete.

Delete a Job

wp perflocale jobs delete <id>

Permanently removes a job from the active-jobs index, deletes its per-job state row, and deletes the export file the job produced, or the stored file of a failed XLIFF import, if it still exists. Only allowed on jobs that have already finished (complete, failed, canceled) - running/queued jobs must be canceled first so the runner has a chance to unschedule cleanly. Matches the same restriction as the REST DELETE /jobs/{id} endpoint.

Pause / Unpause the Queue

wp perflocale jobs pause
wp perflocale jobs unpause

Toggles the background_paused setting. While paused, new dispatches are accepted but workers re-queue them every 5 minutes (configurable) instead of running them, and a job that is already running stops at its next progress step and is re-queued the same way (see Pause Queue). Operator brake when something is mis-dispatching.

Run Garbage Collection

wp perflocale jobs gc

Runs the daily GC sweep right now without waiting for the cron tick. Reports how many jobs were pruned. Performs:

  1. Stuck-job sweep - queued/running jobs with no updated_at bump in 6+ hours are marked failed.
  2. Terminal-state prune - complete/failed/canceled jobs older than 24h are deleted.
  3. Stale lock sweep - perflocale_job_lock_* and perflocale_type_lock_* rows with past expiry are deleted.

Resume Surviving Jobs

wp perflocale jobs resume

Manually trigger the post-reactivation resume sweep that normally runs automatically on activation. Walks the active-jobs index, finds queued/running rows that have no scheduled worker event, and re-enqueues each one. Useful if the auto-scheduled perflocale_resume_jobs cron event was lost (e.g. DISABLE_WP_CRON + no external trigger between activate and the operator noticing). Reports the count re-enqueued.

CLI runs synchronously - admin honors thresholds

The Background Thresholds setting in Settings → Performance only gates admin-triggered operations: when an operator clicks Scan, Import, Migrate, etc., or when an external client hits the REST endpoint, the dispatcher compares the work size against the threshold and routes large operations through the queue.

CLI commands deliberately bypass that gate and run the work inline in the foreground, regardless of size. This matches Unix conventions for command-line tools: progress streams to stdout, errors stream to stderr, exit code reflects success, and chained scripts (wp ... && next-step) behave as expected. Deploy scripts, CI/CD pipelines, and ssh-only servers all rely on this synchronous behavior.

Operations affected: strings scan, translate, export, import, po-import, migrate <plugin>. They will not queue jobs even on very large workloads - you'll see the work happen live and the command will return when it's done. The one explicit opt-out is translate --all --async, which dispatches a resumable site_translate job and returns immediately.

If you need to dispatch a large operation through the queue from a CLI context (e.g. a server cron that should kick off a job and exit immediately so it doesn't tie up the cron worker), use the dispatcher directly:

wp eval '\PerfLocale\Background\Dispatcher::dispatch(
    new \PerfLocale\Background\Jobs\BulkTranslateJob(),
    [ "source_ids" => [ 1, 2, 3 ], "target_lang_ids" => [ 2 ] ]
);'

That returns a job UUID immediately and exits; the queued job runs on the next worker tick (WP-Cron or Action Scheduler, whichever is active).

Circuit Breakers

Inspect and manage PerfLocale's circuit breakers (MT providers, webhooks, FX APIs, geo-IP services). Full subsystem reference: Reliability & Circuit Breakers.

List Breakers

wp perflocale breakers list [--format=<format>]

Options:

  • --format - table (default), json, csv, yaml, count. Machine-readable formats always emit valid parseable output, even when no breakers are tripped.

Examples:

$ wp perflocale breakers list
Success: No active breakers - system healthy.

$ wp perflocale breakers list --format=json | jq '.[] | select(.state=="open") | .key'
"mt_deepl"
"webhook_550e8400-e29b"

$ wp perflocale breakers list --format=count
0

Status of One Breaker

wp perflocale breakers status <key> [--format=<format>]

Returns the full state (state, failures, reason, first/last/opened timestamps, cooldown remaining) for a single breaker. Returns the initial "closed" shape if the key has never tripped.

$ wp perflocale breakers status mt_deepl
+---------------------+---------------------+
| field               | value               |
+---------------------+---------------------+
| key                 | mt_deepl            |
| state               | open                |
| failures            | 5                   |
| reason              | 401_invalid_key     |
| opened_at           | 2026-05-26 10:42:18 |
| cooldown_remaining  | 271s                |
+---------------------+---------------------+

Reset a Breaker

wp perflocale breakers reset <key>
wp perflocale breakers reset --all

Force-closes one breaker (or all of them) immediately. The next call to the underlying service is allowed through; subsequent failures will re-trip the breaker on the normal threshold. Useful when an operator has fixed the upstream problem (e.g. rotated an API key) and doesn't want to wait the full cooldown.

$ wp perflocale breakers reset mt_deepl
Success: Breaker 'mt_deepl' was OPEN and is now closed.

$ wp perflocale breakers reset --all
Success: Reset 3 breaker(s).

Scripting Tips

Suppress Confirmation Prompts

Use --yes to skip all confirmation prompts (useful in scripts and CI):

wp perflocale languages delete fr --yes
wp perflocale translations delete 42 --lang=fr --yes

Combine with WordPress CLI

# Translate all published pages to German
wp perflocale translate --all --to=de --post-type=page --skip-existing

# Check health and fix issues
wp perflocale health-check --fix

# Full maintenance routine
wp perflocale cache flush && wp perflocale slugs backfill && wp perflocale health-check --fix

JSON Output for Parsing

# Get languages as JSON for scripting
wp perflocale languages list --format=json | jq '.[].Slug'

# Get translation status as JSON
wp perflocale status --format=json | jq '.[] | select(.Published < .Total)'