Migrating from WPML

PerfLocale ships a bundled importer for WPML that reads the wp_icl_translations and wp_icl_strings tables and reconstructs your translation groups as native PerfLocale rows. Your WPML data is read-only during the import - nothing is modified or deleted from WPML’s tables, so you can re-run the import, or go back to WPML, at any point.

If you haven’t already, start with the migration overview for the shared pre-flight steps (backup, install PerfLocale, default language and URL layout, add languages).

What’s imported

  • Post translations - every entry in wp_icl_translations with element_type LIKE 'post_%'. This covers posts, pages and all custom post types that WPML was tracking; menu items and attachments are left out (see perflocale/migration/wpml/skipped_element_types). Translation groups are reconstructed from the WPML trid (translation group ID).
  • Term translations - every entry with element_type LIKE 'tax_%': categories, tags, custom taxonomies. WPML’s translation_priority and WooCommerce’s product_visibility taxonomies are left out.
  • Menus - each menu of a WPML menu translation set gets its language, and the menus of the set are linked, so every language shows its own menu in the theme’s menu locations. A menu WPML had in one language only gets no language: it is shown in every language, with its items pointing to each language’s pages. A set whose menus already have another language or link in PerfLocale is left as it is and reported.
  • Order languages - WooCommerce orders with a WPML language get that language in PerfLocale, so their customer emails keep it. An order that already has a PerfLocale language keeps it. Orders whose WPML language has no PerfLocale language are counted in the result, and their emails use the default language.
  • Category and tag assignments - WPML hid a post’s categories and tags that belong to another language, while PerfLocale lists a post under every term it is attached to. The import moves each such assignment to the term’s translation in the post’s language, leaves it where there is none, and reports both counts.
  • String translations - rows from wp_icl_strings joined with wp_icl_string_translations where the translation status is 10 (WPML’s “translation complete”) or 3 (“needs update”). Status 3 is included deliberately: WPML still serves those translations on the front end, just flagged for review, so importing only status 10 would silently drop them. Rows with an empty translated value are skipped whatever their status. The string clean-up leaves the imported text domains alone (see Safety model).

What’s not imported

  • WPML Media Translation copies (per-language copies of attachments). The import leaves attachments out, so those copies stay in the Media Library as separate items that are not linked to the original.
  • WPML’s menu sync, which copies menu changes from one language to the others. After the import, each language’s menu is edited on its own.
  • WPML’s translations of admin texts (the site title, payment gateway titles and other options). They are not applied to those options; translate them again in PerfLocale after the import.
  • Translated URL bases, WPML shortcodes and PHP functions, and “display as translated” - see What does not migrate.
  • WPML Translation Management state (who was assigned what, status history). PerfLocale has no assignment layer - it ships a Translator role and per-translation statuses instead - so there is nothing for that data to map onto.
  • String rows with no translated value - nothing to carry over. They show up as untranslated in PerfLocale → Strings.

Pre-flight checks

In addition to the shared steps in the overview, there are four WPML-specific things to get right, in this order:

Deactivate WPML first

PerfLocale will not start while WPML is active. Activating both leaves you looking at the admin notice “PerfLocale: Cannot run while WPML is active. Please deactivate one of the two multilingual plugins to avoid conflicts.”, a critical Tools → Site Health result, no importer screen, and no wp perflocale migrate command - the CLI commands are registered by the same boot sequence the conflict check stops. So the order is always: deactivate WPML, activate PerfLocale, import, check the result.

Deactivating WPML does not delete anything. Its rows stay in wp_icl_translations and wp_icl_strings, and the importer reads those tables directly, so the data is all still there when the import runs.

If you want to cross-check against WPML’s own editor afterwards, export a list of the posts you care about before you deactivate it - or re-activate WPML once you are satisfied, remembering that PerfLocale stops the moment you do.

Deactivate WooCommerce Multilingual and WPML String Translation as well. They are not needed with PerfLocale, and WooCommerce Multilingual can keep its own currency switcher and currency settings running without WPML. While either is active, PerfLocale’s screens, the Plugins screen and Tools → Site Health show a warning with a Deactivate button for it.

Match WPML’s default language and URL format

A new PerfLocale install starts with English as its default language, and the importer reads WPML’s settings only to check them. If WPML’s default language is another one, set it in PerfLocale before you import:

  1. In PerfLocale → Languages, add WPML’s default language, edit it and tick Default language.
  2. In PerfLocale → Settings → URL & Routing, pick the URL Mode that matches WPML’s language URL format: languages in directories (/fr/) is Subdirectory, a domain or subdomain per language is Per-language domain or Subdomain, and a ?lang= parameter is Query parameter.
  3. Under Default Language URL, keep Hide the language prefix for the default language ticked if WPML gave the default language no directory of its own, and untick it if it did.

Doing this first matters for two reasons. The default language decides which URLs carry no language prefix, so a mismatch moves every URL on the site after the switch. And the importer gives a post or term that exists only in the default language no language record of its own, because PerfLocale reads such content as default-language. If the default were changed after the import, that content would move to the new default language.

The first WPML import checks this for you. It is refused, and writes nothing, when WPML’s default language is not PerfLocale’s default (“WPML’s default language is fr (fr_FR), but PerfLocale’s default language is en. Make fr (fr_FR) the default language under PerfLocale → Languages, then run the import again. Importing now would change the address of every page.”) or when WPML has content in a language PerfLocale does not have. A different URL format, or a different choice about the default language’s directory, gives a warning naming the setting to check. Items in a language WPML had switched off are reported and not imported. Run wp perflocale migrate wpml --dry-run to see the check before you import.

Match your language codes

The importer maps WPML’s language_code (from wp_icl_translations, plus any language that appears only in WPML’s string translations) to a PerfLocale language ID using a two-step lookup:

  1. Exact slug match first. If WPML uses fr, add a PerfLocale language with slug fr.
  2. If no slug match, WPML’s own locale for the code: a PerfLocale language with the same locale matches, ignoring case and hyphens. This is how codes such as zh-hans (WPML locale zh_CN) are mapped.
  3. Then a locale prefix match: PerfLocale’s locale (e.g. fr_FR) starts with WPML’s code (e.g. fr), ignoring case. A hyphen in WPML’s code is read as an underscore, so pt-br matches pt_BR.

A WPML code that none of the steps matches needs a PerfLocale language whose slug is exactly that code. If two WPML codes would match the same PerfLocale language, the first one keeps it and the second is skipped and reported; add a separate PerfLocale language for it and re-run.

Run the import

The string-migration phase runs inside the same idempotent importer, so re-running after a partial run fills in any missing string translations without duplicating posts or terms. Strings are written in batches: on a test copy, 200,000 WPML string translations imported in about a minute through WP-CLI.

From the admin

  1. Go to PerfLocale → Settings → Export & Import.
  2. Scroll to the “Import from WPML” section. If you don’t see it, the wp_icl_translations table isn’t detected - WPML may never have been installed on this site, or the DB prefix is non-standard.
  3. Click Import from WPML. Confirm the prompt.
  4. Wait. On sites with a few hundred multilingual posts the import typically completes in under 30 seconds. The page reloads with a banner showing counts, what was skipped and why, and any per-language warnings. A refused import says what to change.

From WP-CLI

# Sanity-check: confirm WPML data is detected.
wp perflocale migrate wpml --dry-run

# Run the real import (prompts for confirmation).
wp perflocale migrate wpml

# Skip the prompt when scripting.
wp perflocale migrate wpml --yes

For sites with thousands of multilingual posts or string translations, the admin-UI path automatically queues the migration via background jobs (Action Scheduler or WP-Cron) and redirects to PerfLocale → Jobs for live progress. The CLI path is still a good choice if you prefer running migrations under your shell's time budget instead, especially on hosting with strict max_execution_time caps. Only one import runs at a time: a second click while a WPML import is queued or running starts nothing and says so. When the previous WPML import stopped without finishing, it is marked failed and a new one is queued.

Verification

Work through these in order after the import, with WPML still deactivated:

  1. Spot-check a post. Open any multilingual post in Posts → All Posts. The PerfLocale sidebar should list every language that existed in WPML, each linking to the correct translated post.
  2. Check terms. Go to Posts → Categories (or any translated taxonomy) and confirm translated terms are linked in the PerfLocale translation column.
  3. Open a string. Navigate to PerfLocale → Strings and filter for a phrase you remember translating in WPML. Confirm the translation is present for the expected language.
  4. Visit a translated URL. Browse to /fr/your-post-slug/ (adjust for your prefix convention). Confirm the page renders the French content, and that the language switcher highlights French.
  5. View source. Confirm hreflang tags in the page head point at the right sibling URLs.

Re-running the import

The WPML importer is idempotent: re-running it after a partial or failed run completes the gaps rather than creating duplicates. Internally, every trid is checked against existing PerfLocale translation groups - if a group already contains any of the posts from that trid, the importer reuses it and links the remaining siblings. If a sibling is already linked to the same group with the same language, it’s skipped silently (no cache churn).

A WPML set that conflicts with what PerfLocale already holds is not relinked. It is left as it is and reported: when one of its items already belongs to the group of another WPML set, when linking it would unlink another item that holds one of its languages, or when its items sit in different PerfLocale groups that also hold other items. When WPML’s table was rebuilt and its trid numbers changed, a re-run maps the new sets onto the groups the first import made.

If a later run reports a language PerfLocale does not have, add the missing language and re-run: the content already imported stays intact and the new language’s content fills in.

Rollback

There’s no “undo migration” button, and none is needed for WPML’s data. The importer creates no posts or terms: it links the posts and terms WPML already made, and writes to PerfLocale’s own tables and files, the language of menus and orders, and the category and tag assignments it moves (see What’s imported). WPML’s tables are not changed. Two ways back:

  • Switch back to WPML - deactivate PerfLocale and reactivate WPML. WPML finds its tables as it left them. Changes you made to posts while PerfLocale was running stay, as with any edit.
  • Restore from backup - rewinds everything to the moment of the backup, including the wp_perflocale_* rows the import wrote.

Do not delete translated posts to undo the import. Every translated post on the site is one of your WPML translations, not a copy made by the importer, so deleting it deletes real content.

Keep WPML installed but deactivated until you’re certain you won’t need it.

Troubleshooting

“No matching languages found between WPML and PerfLocale”

Every language in wp_icl_translations.language_code has to match a PerfLocale language (see Match your language codes). Check the output for “WPML has content in <x>, which has no matching PerfLocale language” messages, or run wp perflocale migrate wpml --dry-run, then add the missing languages in PerfLocale → Languages and re-run.

The import timed out

Large migrations from the admin UI automatically queue via background jobs and complete in the background, so a timeout should not arise. If it does (e.g. on a host where both WP-Cron is disabled AND Action Scheduler isn't loaded AND no external cron is configured), switch to WP-CLI - it runs under your shell's time budget and can handle sites of any size.

Memory on very large sites

The WPML importer fetches translation rows in batches rather than loading every trid into memory at once. It first pulls the distinct trid list (cheap - one BIGINT per row), chunks it by 100 by default, and only reads the (trid, element_id, language_code, element_type) rows one chunk at a time. On a 50 000-post site the peak memory delta stays within a couple of MB regardless of how many translation groups exist.

You can tune the batch size via a filter - useful in two directions:

  • Tight memory budget (shared hosting, low memory_limit): lower the value to e.g. 25 for more, smaller fetches.
  • Plenty of RAM, latency-sensitive: raise it to e.g. 500 for fewer SQL roundtrips.
add_filter( 'perflocale/migration/wpml/batch_size', static fn(): int => 250 );

Clamped to 10–1000; default 100. See perflocale/migration/wpml/batch_size in the hooks reference.

I re-ran and now I have duplicate posts

You shouldn’t - the importer is idempotent. If you’re seeing duplicates, they were most likely created by WPML itself (WPML sometimes creates “duplicate” placeholder posts when a translation is queued). Check whether the duplicates existed pre-import via Posts → All Posts filtered by language. If they did, they’re a WPML artefact, not a migration bug.

Next steps