Export & Import

Translations need to travel: between staging and production, to an external translation agency, to a colleague’s computer for review, to backup storage. PerfLocale supports three formats covering every common use case - pick the one that fits the receiver. Each has its own home in the admin, so it’s worth knowing which screen to open.

Formats

  • JSON - PerfLocale’s own format, and the one the Settings → Export & Import screen speaks. Round-trips everything the plugin owns: settings, role capabilities, languages, translation link groups, strings and their translations, translated slugs, content hashes, per-language media texts, classic menu languages, per-item flags and add-on data such as Gravity Forms translations. Use it for backups and restores of a site, for copies of a site (staging ↔ production), and for moving settings, languages and strings to any site. The sections that name posts and terms by ID need the same-site or same-copy relationship - see what travels between sites.
  • .po - classic gettext format, for string translations only. Export and import it from PerfLocale → Strings (the Export PO / Import PO buttons next to the page heading) or via WP-CLI, then edit in Poedit or Loco.
  • XLIFF 2.0 - the industry-standard translation exchange format for handing post content to professional translators. Open in any CAT tool (SDL Trados, memoQ, Phrase, Crowdin, etc.). Post titles, content, and excerpts travel as <unit> / <segment> elements per the XLIFF 2.0 schema (urn:oasis:names:tc:xliff:document:2.0). XLIFF has no admin screen of its own - it is exposed through the two REST routes described below.

Exporting via the admin UI

At Settings → Export & Import, the Export row lists one checkbox per section - Settings, Roles & Capabilities, Languages, Translation Links, String Translations, Slug Translations, Content Hashes, Media, Menus, Item flags, Add-on data - all ticked by default, with a Select All toggle above them. Untick what you don’t need and click Export Data. The result is always a single JSON file.

  • Media - each language’s alt text, caption and description of a media item, and a post’s featured image per language.
  • Menus - a classic menu’s language and its pairing with its translations.
  • Item flags - Independent across languages, Hide from hreflang and sitemap alternates, and the language of a WooCommerce order.
  • Add-on data - data that add-ons store, such as the Gravity Forms translations. An add-on can give its data a checkbox of its own, listed after Add-on data; the data of every other add-on goes with the Add-on data checkbox.
  • Languages also carries the redirects of renamed language codes, Settings the webhooks, and Roles & Capabilities the Translator role’s permissions. Webhooks and permissions are applied by a Replace import only.

When a full export of the site would be larger than the site accepts for an import through this screen, the Export row says so before you export, with the ways to restore it: “A full export of this site will be about …, more than this site accepts for an import through this screen (…). To restore it, import it with WP-CLI (wp perflocale import), raise the import limit, or export the sections in smaller files.”

Small exports stream directly to your browser; large ones queue as a data_export background job and the admin redirects to PerfLocale → Jobs. When the job completes, a single-use Download button appears on the job row - click it and the export file is streamed to your browser and deleted from the server in the same request. The async/sync split is governed by the Data export (JSON) threshold under Settings → Performance → Background Thresholds (default: 5,000 rows). An export that one web request cannot finish on your host is refused with the command to run instead, wp perflocale export <file> (see Background Jobs).

Importing via the admin UI

Same page, Import section. Drop in (or browse for) a PerfLocale JSON export, choose Merge or Replace, and submit. Merge adds the file’s rows and leaves this site’s settings, add-on settings, add-on list and roles as they are; Replace clears this site’s rows for the sections the file declares, loads the file and applies its configuration (see Merge vs. replace). The importer validates the envelope before writing and refuses a file whose format_version it doesn’t understand.

Add-on data to import. Under the mode choice, one checkbox per kind of add-on data chooses what the import restores: Add-on data for the data of every add-on without a checkbox of its own, and one checkbox for each add-on that has one. All are ticked by default. Add-on data whose add-on is not active on this site is skipped and named in the import report; for example, the Gravity Forms translations are imported only when Gravity Forms and its PerfLocale add-on are active. wp perflocale import --addon-data makes the same choice (see WP-CLI).

File size. The screen accepts files up to 512 MB by default (the perflocale/import/max_file_bytes filter changes that), and shows the limit under the file field. A file larger than PHP accepts in one request is uploaded in pieces, with the progress shown under the form; when the server or a proxy refuses a piece as too large, or the request ends with no answer, the screen sends it again in smaller pieces, down to 256 KB. The server first cuts its copy of the file back to where that piece starts, so a piece it had already written in full or in part, for example when the connection broke before its answer arrived, is not stored twice and the upload continues. The importer then reads the file in pieces too, on this screen, with wp perflocale import and with wp perflocale network-import, so a file’s size does not decide the memory an import needs. WP-CLI has no file size limit. If an upload stops, nothing is imported, and the status line says why, for example “The upload stopped and nothing was imported: the connection to the server was lost.”

Limits. The importer refuses, before it writes anything, a file whose data it cannot import safely: more languages, rows per table, or bytes in one value, row or section than the perflocale/import/quality_limits filter allows. The default number of rows per table follows the memory PHP may use (memory_limit) and is never below 500,000, so a genuine export within the size limit is not refused for having many rows. The refusal says which limit the file passed, for example “Table … has … rows, more than the … this importer accepts per table. A developer can raise the limit (max_rows_per_table) with the perflocale/import/quality_limits filter.”

Rows left out. Every row the import leaves out is counted and named with its reason, for example a row whose language does not exist on this site, or whose post or term is a different item here: “3 rows were not imported because the language they name does not exist on this site. Import the file with its Languages section to bring the language too.”

Sync Fields. An import’s own writes do not start Sync Fields mirroring, in Merge and in Replace: each post gets the values the file holds for it, so the import reproduces the file. The perflocale/import/sync_fields_mirroring filter turns mirroring on for imports.

One import at a time. A site runs one data import at a time, whether it was started from this screen, as a background job, with wp perflocale import or as a slice of wp perflocale network-import. Another import started meanwhile 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.”

Below the mode radios sits one checkbox: This site is a copy of the site the file was exported from (post and term IDs match). Leave it unticked for an ordinary backup restore. Tick it only when this site really is a copy of the exporting one - a staging clone, a domain migration - because it waives the check described under what travels between sites.

Large imports queue automatically via PerfLocale's background-jobs system (Action Scheduler when loaded; WP-Cron otherwise) and the admin redirects to PerfLocale → Jobs for live progress. Small operations still finish inline. A file that one web request cannot import on your host is refused with “This file is larger than this site imports through this screen (…). Import it with WP-CLI instead: wp perflocale import <file>.” The uploaded file is persisted under wp-content/uploads/perflocale/temp/ for the worker to read and is cleaned up on completion.

Imports by accounts without unfiltered_html

An import started from this screen is stored at the trust level of the account that started it. For an account without WordPress’s unfiltered_html capability, three sections are cleaned with the rules PerfLocale applies when it saves the same data itself:

  • String Translations - each translation, and each extra plural form, goes through the HTML filter the Strings screen applies when a translation is saved (WordPress’s wp_kses_post()), which keeps the markup WordPress allows in post content. A value with no < in it is stored exactly as the file has it. A translation the filter empties is still stored, empty, and the site treats it as untranslated. A PO upload on the Strings screen gets the same filter - see String Translation.
  • Languages - every text value of a language row is cleaned the way the Languages screen cleans it when a language is saved: HTML tags are removed; the code is lower-cased and keeps only letters, digits, hyphens and underscores, up to 10 characters; and the locale is cut to 20 characters and the flag to 10. A language whose code has nothing left after that is not imported, and the import reports The language with export ID … was not imported. In Replace mode that is a failed row, so the whole import is rolled back. In Merge mode the rest of the file is imported, and the rows that belong to that language are attached to this site’s language with the same locale, if there is one; without one, they are left out, as are the rows of any parent row that does not import, and the report counts them: “… rows were not imported because the rows they belong to could not be saved (see the errors above).”
  • Slug Translations - each translated slug goes through sanitize_title(), as every slug PerfLocale writes does (see how translated slugs are stored). A slug with nothing left is stored empty, which PerfLocale reads as no translation, so the object’s own slug is used in its URLs.

Because the values are stored the way PerfLocale stores them when it saves them itself, a value with no markup in it can change too: a date format with a double space loses one (j  F Y becomes j F Y), an empty text direction becomes ltr, and a slug is sanitized again.

Every stored row the cleaning changed is counted once. An import that finishes on this screen says so in its notice - … imported items were changed by the filters that apply to accounts without the unfiltered_html capability … - including when the import also reported errors, because those rows were stored either way. A queued import records the count as sanitized in the job’s result, shown in the job’s details under PerfLocale → Jobs. Developers get the same figure in the result the perflocale/import/completed action receives.

Accounts that hold unfiltered_html - on a single site that includes administrators, unless DISALLOW_UNFILTERED_HTML is set - store the text in these three sections exactly as the file has it. wp perflocale import and network-import always do, whichever user they run as.

WP-CLI (faster for large exports)

For sites with tens of thousands of translations, the CLI is still a strong choice (streams directly to disk, runs under your shell's time budget, easy to schedule). Each subcommand writes a single file; format is dictated by the subcommand, not a flag:

# Full-site JSON envelope (everything PerfLocale owns - settings, role caps,
# languages, translation links, strings, slugs, content hashes, media,
# menus, item flags, add-on data).
# The envelope embeds a `format_version` field so older importers refuse
# mismatched payloads.
wp perflocale export /tmp/full-backup.json
wp perflocale import /tmp/full-backup.json                      # additive merge
wp perflocale import /tmp/full-backup.json --mode=replace       # truncate-then-load
wp perflocale import /tmp/staging.json --force                  # this site is a copy of the exporting one
wp perflocale import /tmp/full-backup.json --addon-data=none     # leave the add-on data out

# Gettext PO files for string translations, one language at a time
wp perflocale po-export /tmp/de.po --lang=de
wp perflocale po-export /tmp/de-mytheme.po --lang=de --domain=mytheme
wp perflocale po-import /tmp/de.po --lang=de
wp perflocale po-import /tmp/de.po --lang=de --replace

# Multisite: one envelope keyed by blog ID, all sites at once
wp perflocale network-export /tmp/network.json
wp perflocale network-export /tmp/network.json --include-inactive
wp perflocale network-import /tmp/network.json --mode=replace
wp perflocale network-import /tmp/network.json --site=42        # restore one slice on a single-site target
wp perflocale network-import /tmp/network.json --force          # slices whose recorded address no longer matches

# `wp perflocale import` reads PerfLocale JSON envelopes only. XLIFF goes
# through the REST routes below, not through the CLI.

See WP-CLI Commands → Export & Import for the complete flag reference.

Merge vs. replace semantics

  • Merge (default) - additive. Existing rows are kept, and a row that points at a parent the same bundle carries is re-pointed at the row that parent actually landed on. Note that re-importing the same backup in Merge mode allocates fresh translation_groups rows, because that table has no natural key to match on; the accompanying links de-duplicate through their object_lang (type, object_id, language_id) UNIQUE key, so the surplus groups end up linked to nothing and are swept at the end of every Merge import. Harmless, but not free. Merge never changes this site’s configuration: the file’s settings, add-on settings, disabled-add-on list and role capabilities are not applied. Each of those the file carries is counted as skipped, and the import notice, the job’s details under PerfLocale → Jobs and WP-CLI (a warning naming --mode=replace) say that they were not applied. Merge never changes this site’s default language: when the file has another default, that language is added as an ordinary language and the import says so. A post or term that already has a language here keeps it: a translation link that would give it a second language is left out and counted.
  • Replace - clears the target tables (scoped to the current site on multisite, and to the sections the bundle declares - a table it declares but carries no rows for is still cleared, with one exception: String Translations clears its translation groups, which share a table with Translation Links, only when the file carries some) before loading, then restores every row under the ID it was exported with. Nothing is re-mapped, which is what lets a same-site restore put every row back under its original ID. The one exception: when a file replaces Translation Links without String Translations, the strings this site keeps stay where they are, so a post or term group, or a post or term link, whose ID a kept string already holds is written under a new ID (a group's links follow it). Data outside the tables (item flags on posts, terms and orders, menu and media languages, redirects, add-on data) is overwritten only where the file has a value; what the file does not hold is kept. Use it when you genuinely want to wipe the destination's PerfLocale state first, and for repeatable, deterministic re-imports. Replace also applies the file’s settings, add-on settings, disabled-add-on list and role capabilities. A file that carries only the Settings and Roles & Capabilities sections owns no table, so a Replace import of it deletes no rows: that is how to apply a configuration from a file. Replace keeps the settings that describe this site rather than its translations: its per-language domains, its machine-translation provider addresses, the uninstall choice and a paused background queue (the perflocale/import/keep_target_settings filter changes the list). The import names each one whose value in the file was different: “Kept this site’s own value for: … These settings belong to the site, so Replace does not take them from the file; change them under PerfLocale → Settings if needed.” Webhooks the file carries get a new secret unless this site already has a webhook at that address; the import says how many to register again with the secret their receivers expect.

Replace refuses a file from another site. Each row that names a post, term or order by ID carries what identifies that item, such as its GUID, slug, date or name. When more than half of those rows name a different item on this site, the file most likely comes from another site, or from this site before its content was rebuilt, and Replace would remove this site’s own rows for those sections; it refuses the file and changes nothing. Merge skips such rows and imports the rest. A row whose item changed since the export but is still recognised, for example a renamed slug, is imported, and the report says what it was recognised by.

Replace is all-or-nothing. The wipe and the reload run inside one database transaction, and 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, or one whose process is stopped part-way, changes none of them. You get your pre-import state and an error naming the table, rather than a table that is missing the rows that failed. The same applies to a PO import in Replace mode (po-import --replace, or Import PO on the Strings screen): a refused translation rolls the language back instead of leaving it part-replaced, with “Replace import rolled back: the database refused a write, so nothing was changed.” An import that adds or removes languages rebuilds the language URL prefixes.

Because of that, an import that reports errors is a failed import. wp perflocale import, po-import and network-import all exit non-zero when they do, so a restore script can rely on the exit code alone.

XLIFF handoff to a translator, step by step

XLIFF has no admin screen. You export and import it with two REST API requests, from a terminal or any HTTP client, signed in with a WordPress Application Password. The steps below send two English posts to a translator as one XLIFF file and import the German file that comes back. The commands are shown as we ran them on a test site: put in your own site address, user name, password and post IDs.

The two requests at a glance

Part of the requestExportImport
MethodPOSTPOST
URL/wp-json/perflocale/v1/xliff/export/wp-json/perflocale/v1/xliff/import
BodyJSON with post_ids (required, an array of source post IDs), target_lang (required, a language code such as de) and source_lang (optional, default en).xliff: the text of the translated file, as a field of a JSON body or as a form field. There is no file-upload field.
ResponseJSON with xliff (the XLIFF 2.0 file, as text) and filename (a suggested file name such as perflocale_en_de_20261016.xliff).JSON with imported, skipped and errors (see step 4).

Both requests sign in with HTTP Basic authentication: your WordPress user name and an Application Password. The account needs the perflocale_import_export capability (Administrators have it by default) and must be allowed to edit every post in the request or the file. The import takes the target language from the file’s trgLang attribute, so it has no target_lang parameter.

1

Create an Application Password

You, once

In wp-admin, go to Users → Profile and scroll to Application Passwords. Type a name such as XLIFF handoff in New Application Password Name and click Add Application Password. Copy the password WordPress shows: it is shown only once. You can keep its spaces or leave them out.

Or create it with WP-CLI (replace admin with your user name):

wp user application-password create admin "XLIFF handoff" --porcelain
Application Passwords need HTTPS. On a live site, WordPress offers them only when the site runs over HTTPS. Over plain http they are available only when the site’s environment type is local (define( 'WP_ENVIRONMENT_TYPE', 'local' ); in wp-config.php), which is meant for a development copy on your own computer.

Then set three shell variables. Every command below reads them. The examples are written for a Bash shell and use curl and jq.

SITE="https://example.com"
WP_USER="admin"
WP_APP_PASSWORD="abcd EFGH ijkl MNOP qrst UVWX"
2

Export the posts

You

Use the IDs of the posts in the source language. A post’s ID is the number after post= in the address of its edit screen. This example exports posts 4 and 5 from English (en) for German (de):

curl -sS -u "$WP_USER:$WP_APP_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{"post_ids":[4,5],"source_lang":"en","target_lang":"de"}' \
  -o export.json \
  "$SITE/wp-json/perflocale/v1/xliff/export"
jq -r '.xliff' export.json > perflocale_en_de.xliff

curl saves the JSON response as export.json, and jq writes its xliff field to perflocale_en_de.xliff: that file is what you send to the translator. The response’s filename field suggests a name with the date in it; any name works.

No jq? Replace the last line with this one. It writes the same file, byte for byte:

python3 -c 'import json; print(json.load(open("export.json"))["xliff"])' > perflocale_en_de.xliff

The file has one <file> element per post and one <unit> per field: the title, and the content and excerpt when they are not empty. The start of the exported file:

<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
 <file id="post-4" original="post-4">
  <unit id="post-4-title">
   <segment>
    <source><![CDATA[About Us]]></source>
    <target/>
   </segment>
  </unit>
  <unit id="post-4-content">
   <segment>
    <source><![CDATA[<!-- wp:paragraph -->
<p>We build performance-first WordPress plugins for teams that ship to multiple markets.</p>
<!-- /wp:paragraph -->]]></source>
    <target/>
   </segment>
  </unit>
 </file>
3

Translate the file

Your translator

The translator opens the file in a CAT tool (SDL Trados, memoQ, Phrase, Crowdin and others read XLIFF 2.0) or a text editor, writes each translation into the <target> of its unit, and sends the file back. One unit before and after:

<unit id="post-4-title">
 <segment>
  <source><![CDATA[About Us]]></source>
  <target/>
 </segment>
</unit>
<unit id="post-4-title">
 <segment>
  <source><![CDATA[About Us]]></source>
  <target><![CDATA[Über uns]]></target>
 </segment>
</unit>
  • Keep the id of every unit and the trgLang attribute as they are. A unit id such as post-4-title names the source post and the field; trgLang names the language the text is imported into.
  • Keep the markup. Text with HTML goes inside <![CDATA[ ... ]]>, as in the source. Keep the block comments, such as <!-- wp:paragraph -->, where they are, so the block editor still sees the same blocks.
  • An empty <target> is skipped. A partly translated file imports the units that have text, and an empty unit changes nothing.
  • Only the <target> text is imported. The <source> text is not read.
4

Import the translated file

You

Send the text of the file as the xliff field. As a form field:

curl -sS -u "$WP_USER:$WP_APP_PASSWORD" \
  -F 'xliff=<perflocale_en_de.xliff' \
  "$SITE/wp-json/perflocale/v1/xliff/import"
Write xliff=<, with a less-than sign. The < makes curl read the file and send its text as the xliff field: it is not part of the file name, and the single quotes around it must stay. With @ instead, curl sends a file upload, which this endpoint does not read, and the answer is 400 missing_xliff.

Or as a JSON body, built with jq:

jq -n --rawfile xliff perflocale_en_de.xliff '{xliff: $xliff}' \
  | curl -sS -u "$WP_USER:$WP_APP_PASSWORD" \
      -H "Content-Type: application/json" \
      --data-binary @- \
      "$SITE/wp-json/perflocale/v1/xliff/import"

Both answer with the same counts. For the example file, five units were written:

{"imported":5,"skipped":0,"errors":[]}
  • imported - units written to the translations.
  • skipped - units with an empty <target>, units whose id is not post-{ID}-title, post-{ID}-content or post-{ID}-excerpt, and earlier copies of a unit id the file repeats (only the last copy is applied).
  • errors - one message for each post whose units could not be written, with the post ID, the unit ids and the reason, for example that the post does not exist on this site or that you may not edit it. The other units are still imported.

A file with errors still answers with HTTP 200, so check errors. This is the answer for a file whose three post-999 units name a post that does not exist:

{"imported":2,"skipped":0,"errors":["Post 999 (units post-999-title, post-999-content, post-999-excerpt): this post does not exist on this site. The file may come from another site, or the post was deleted."]}

Importing the same file again updates the same translations; it does not create new ones.

5

Review and publish

You, in wp-admin

Go to Posts and choose German in the language filter above the list. For each post that had no German translation, the import created one with the translated title, content and excerpt, as a draft (or pending review, when that is your Default Translation Status). A translation that already existed is updated and keeps its status. The English posts are not changed.

Open each draft in the editor, check it, and publish it when you are happy with it.

Content and excerpts imported by an account without WordPress’s unfiltered_html capability pass through the same tightened HTML allowlist PerfLocale applies to machine-translation output, so a file from outside cannot add markup that WordPress would not let that account save.

Errors

Errors come back in the standard WordPress REST shape, {"code": ..., "message": ..., "data": {"status": ...}}, with the same HTTP status.

StatusCodeCause
400invalid_paramsExport without post_ids or target_lang.
400missing_xliffImport without an xliff field, or with the file sent as an upload (-F 'xliff=@file').
400invalid_xliffThe importer cannot read the file: it is empty, is not well-formed XML, declares XML entities, or its trgLang matches no active language. Retrying will not help; fix the file.
413invalid_xliffThe file is larger than 32 MB, has more than 60 translation units, or has more XML elements or attributes than one import reads. The message names the limit, for example “XLIFF import refused: the file has more than 60 translation units, the limit for one import. Export fewer posts per file and import each file on its own.” Nothing is imported.
401rest_forbiddenNo user name and password were sent, or they are wrong.
403rest_forbiddenThe account lacks perflocale_import_export, or (export) may not edit one of the posts.
500export_failed, import_failedSomething failed on the server, for example a missing PHP XML extension. The message says what.

For example, a file that was cut short:

curl -sS -u "$WP_USER:$WP_APP_PASSWORD" \
  -F 'xliff=<broken.xliff' \
  "$SITE/wp-json/perflocale/v1/xliff/import"
{"code":"invalid_xliff","message":"Invalid XLIFF XML: StartTag: invalid element name; Premature end of data in tag segment line 5","data":{"status":400}}

What isn’t included in exports

The exporter ships everything you need to reconstitute a site’s multilingual configuration and translation data. A small set of things is deliberately left out, because including them would either leak secrets, queue meaningless work on the destination, or duplicate state that’s regenerated automatically:

  • API keys / tokens / secrets / passwords - every setting whose name marks it as a credential (API keys, tokens, secrets, passwords, credentials and encrypted values), in the main settings and in each add-on’s settings, is left out of the file, and so is every add-on setting its add-on declares as a password field, whatever its name. A Replace import keeps the destination’s own value of each such setting. The destination operator re-enters them (or supplies them via wp-config.php constants / env vars). This is the right behavior for security: a JSON file shouldn’t round-trip your DeepL key through email.
  • Webhook secrets - webhooks travel in the Settings section without their secrets. On import, a webhook whose address this site already has keeps this site’s secret; any other gets a new random one, so register it again with the secret its receiver expects.
  • Background-jobs queue state - the jobs table holds operational state with source-site-specific user / post / blog IDs. Importing it would queue meaningless work on the destination. The destination starts with an empty queue and dispatches its own.
  • Per-user admin UI preferences - user-meta keys like perflocale_strings_hidden_langs are user-id-specific and not portable across sites with different user tables.
  • Generated translation files - the .l10n.php output files in uploads/perflocale/translations/ are regenerated from strings + string_translations on the destination. Shipping the build artifacts would multiply export size for no benefit.
  • Internal caches - the addon-failure tracker, the active-engine memo, the eager-link-map, and similar diagnostic / runtime options are auto-rebuilt from the data that is exported.

What travels between sites, and what doesn’t

Some references are resolved by content rather than by number. In Merge mode the importer looks a language_id up by the language’s slug, falling back to its locale, and a string_id up by the string’s original_hash. So a German row exported from a site where German has id = 3 lands correctly under whatever id German happens to have on the destination. Both resolutions need the section that owns the parent to be in the same bundle: rows exported without Languages have nothing to look their language up against, which is why the export screen tells you to keep that box ticked.

Post and term IDs are different: there is no content key to match them on. translation_links, slug_translations and content_hashes name their object by numeric object_id, and the importer stores that number exactly as exported. On a site that was set up separately, the same numbers address unrelated posts and terms - so the import would quietly group unrelated content as translations of one another, and the switcher and hreflang tags would follow.

Because of that, a bundle carrying those rows is refused when the site address recorded in the file is not this site’s. So is a bundle from another address whose translations arrive without the Languages section. And in Replace mode a bundle from another address is refused for the sections it declares, whether or not it carries rows for them - Replace deletes this site’s rows for those sections before it loads anything, so an otherwise empty file from another site would empty your tables. Every refusal happens before anything at all is written - no settings, no role grants, no rows - and names both addresses. The fix is usually to export again from the right site, or with Languages ticked.

On the import screen the refusal comes back as an error. A large import that had already been queued ends as a failed job under PerfLocale → Jobs, carrying the same sentence - but not immediately: a failing job is retried with backoff, so the row sits at queued for several minutes before it settles on failed and shows the reason. Each refusal message leads with what to do about it, because that list shows only the first line or so beside the row and keeps the rest in the tooltip.

When this site genuinely is a copy of the exporting one - a staging clone, a domain migration - tick This site is a copy of the site the file was exported from on the import form, or pass --force to wp perflocale import / network-import. Tick it as well for a backup of this site that records a language-specific address - a language’s subdomain or own domain, or a language-prefixed home URL - instead of the site address: the check compares the recorded address with this site’s own address, so such a backup does not match. Note also that a matching address is not proof by itself: a site rebuilt from a WordPress content export keeps its address and gets fresh post IDs, and the check cannot see that.

From your own code, DataImporter::import() takes an options array whose allow_foreign_ids key does the same as ticking the box or passing --force.

A bundle that records no address at all is not checked - that is what a hand-made file looks like. One that records an address the plugin cannot read counts as a mismatch.

In Merge mode, bundles that carry only settings, role capabilities or languages are never refused (Merge imports the languages and applies none of the settings or role capabilities), and neither are string bundles that travel with their Languages section - those sections carry no post or term IDs at all. In Replace mode the same bundles are refused unless you confirm the copy, because of the deletion Replace performs first; only a settings-or-roles-only bundle, which owns no table, is exempt in both modes.

Replace across sites is not supported, even when you confirm the copy. A cross-site Replace that carries Languages restores the exporting site’s language IDs, and any row on this site that the bundle does not itself replace keeps pointing at the ID those numbers used to mean, so it can end up attached to the wrong language. Use Replace to restore a backup of this site; to bring in translations from a copy of this site, use Merge and confirm the copy.

Format version + compatibility

Every export carries a format_version integer (currently 1). The importer accepts the current version and any older version it knows how to migrate. It refuses newer versions with a clear error rather than risk silent data loss on a partial-format-understanding import. Upgrade the destination plugin first if you see that error.

← Back to Docs