---
title: "Migrating from WPML to PerfLocale - PerfLocale"
description: "Step-by-step guide to importing WPML posts, terms, and strings into PerfLocale. Admin UI and WP-CLI paths, safety guarantees, verification."
canonical: "https://perflocale.com/docs/migration/wpml/"
source: "https://perflocale.com/docs/migration/wpml/"
format: "markdown"
---
# 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, abandon, or verify the migration at your own pace before switching over.

If you haven’t already, start with the [migration overview](https://perflocale.com/docs/migration/) for the shared pre-flight steps (backup, install PerfLocale, add languages).

## What’s imported

-   **Post translations** - every entry in `wp_icl_translations` with `element_type LIKE 'post_%'`. This covers posts, pages, attachments, and all custom post types that WPML was tracking. Translation groups are reconstructed from the WPML `trid` (translation group ID).
-   **Term translations** - every entry with `element_type LIKE 'tax_%'`: categories, tags, custom taxonomies.
-   **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 silently dropped them. Rows with an empty translated value are skipped whatever their status.

## What’s _not_ imported

-   **WPML Media Translation uploads** (per-language media files). PerfLocale handles multilingual media differently - via the same post-translation-group mechanism applied to attachments - so re-translating media is a post-migration task in the PerfLocale admin.
-   **WPML Menu Sync relationships** that rely on WPML-internal IDs. Menus themselves are translated as terms (via the `nav_menu_item` post type) when WPML tracks them that way, but the sync-on-save logic is a WPML-specific feature.
-   **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](https://perflocale.com/docs/migration/), there are two WPML-specific things to get right:

### Match your language codes

The importer maps WPML’s `language_code` (from `wp_icl_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, **locale prefix** match: PerfLocale’s `locale` (e.g. `fr_FR`) starts-with WPML’s code (e.g. `fr`). Useful when WPML uses a short code and PerfLocale tracks a full locale.

If your WPML site used a non-standard code (e.g. `en-US` with a hyphen instead of an underscore), add a PerfLocale language with the matching slug exactly.

### 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.

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.

## 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.

### 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 and any per-language warnings.

### 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, the admin-UI path automatically queues the migration via [background jobs](https://perflocale.com/docs/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.

## Verification

Work through these in order before deactivating WPML:

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).

This means if your first run warned about “No PerfLocale language match for WPML code <x>”, you can add the missing language and re-run - the previously-imported content stays intact and the new language’s content fills in.

## Rollback

There’s no “undo migration” button. Two paths if you need to back out:

-   **Restore from backup** - the cleanest undo, since it rewinds any new `wp_perflocale_*` rows that were written.
-   **Manual cleanup while WPML is still active** - if you only imported a handful of posts, you can open each newly-created translation post and delete it from _Posts → All Posts_. The original WPML-managed posts are untouched.

In either case, leave WPML activated until you’re certain you won’t need it - WPML’s tables are not modified by the importer, so reactivating it restores the pre-migration experience immediately.

## Troubleshooting

### “No matching languages found between WPML and PerfLocale”

Every language in `wp_icl_translations.language_code` has to match either a PerfLocale language slug or the prefix of a locale. Check the output for specific “No PerfLocale language match for WPML code <x>” warnings, then add the missing languages in _PerfLocale → Languages_ and re-run.

### The import timed out

As of 1.0.0, large migrations from the admin UI automatically queue via [background jobs](https://perflocale.com/docs/background-jobs/) and complete in the background — the original timeout case shouldn't arise anymore. 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.

```php
add_filter( 'perflocale/migration/wpml/batch_size', static fn(): int => 250 );
```

Clamped to `10–1000`; default `100`. See [`perflocale/migration/wpml/batch_size`](https://perflocale.com/docs/hooks/#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

-   Configure [language fallbacks](https://perflocale.com/docs/language-fallbacks/) - untranslated posts can redirect to a related language instead of 404’ing.
-   Scan for theme/plugin gettext strings via [`wp perflocale strings scan`](https://perflocale.com/docs/wp-cli/#scan-for-strings) - WPML’s string table only covers what WPML’s own scanner picked up.
-   See the [PerfLocale vs WPML feature comparison](https://perflocale.com/compare/wpml/) for what’s available in PerfLocale beyond what WPML offers.
