Migrating from Polylang

PerfLocale’s Polylang importer reads the post_translations and term_translations taxonomies and rebuilds your translation groups as native PerfLocale rows, then imports what Polylang keeps beside them. Polylang’s data is read-only during the import - nothing is touched in its tables or settings.

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

What’s imported

  • Post translations - Polylang stores translation groups as serialised PHP arrays in the description field of each post_translations term. The importer unserialises those, looks up each post by ID, and rebuilds the group in PerfLocale.
  • Term translations - same approach with the term_translations taxonomy: categories, tags, and any custom taxonomies Polylang was tracking.
  • Menus - the menu each theme location showed per language gets that language, and the menus of a location are linked, so every language shows its own menu. A location whose menus already have another language or link in PerfLocale is left as it is and reported. A menu Polylang showed for two languages stays with the first of them.
  • Site title, tagline and formats - Polylang’s translations of the site title and tagline become PerfLocale’s translations of them, and its translated date and time formats become each language’s own formats when that language has none yet.
  • Translatable types - post types and taxonomies Polylang translated are made translatable in PerfLocale (Settings → Translation), and the result names them. Attachments stay out.
  • Per-language prices - when the language versions of a WooCommerce product have different prices, as Polylang allowed, the first import switches off PerfLocale’s Price Sync so the first edit does not copy one language’s prices to the others, and says so. A later import leaves the setting as you chose it and warns instead.

What’s not imported

  • Other string translations. Widget titles and texts, and strings a theme or plugin registered with Polylang, have no place in PerfLocale. The import counts them per language. After the migration, scan the site with wp perflocale strings scan and translate the theme and plugin strings you need in the PerfLocale admin.
  • Widget language settings. Polylang’s per-language widget settings are not carried over.
  • Polylang’s language-switcher menu item, and anything else in What does not migrate.
  • Language-switcher widget configuration. Polylang’s widget settings don’t carry across - configure PerfLocale’s Language Switcher block, widget, or shortcode from scratch.
  • Custom language flags and display names. Re-add them in PerfLocale → Languages when you create the matching PerfLocale language.

Pre-flight checks

Polylang doesn’t have to be active

The importer works both with Polylang active and with Polylang deactivated. Detection is two-layered:

  1. If the language taxonomy is registered (Polylang is active), the importer uses it.
  2. If Polylang is deactivated but its data remains in the database (language terms in wp_term_taxonomy), the importer detects that and proceeds anyway.

Practically: if you already switched off Polylang, the migration still works - the importer detects the data, not the plugin.

Leave it off, though. PerfLocale refuses to start while Polylang is active, so re-activating Polylang to help with verification stops PerfLocale instead: you get the admin notice “PerfLocale: Cannot run while Polylang is active” and no plugin screens at all. Verify from PerfLocale’s own admin and the front end.

Match your language codes

The importer maps Polylang’s language slug (e.g. fr, de) to a PerfLocale language using:

  1. The importer reads each Polylang language term’s authoritative WordPress locale (stored serialized in the term’s description, e.g. pt_BR) and matches it to a PerfLocale language whose locale is the same after normalizing case and hyphens-vs-underscores (so Polylang pt-br matches PerfLocale pt_BR, and pt_BR never collapses into pt_PT).
  2. If no locale is recorded, it falls back to an exact slug match (Polylang slug equals the PerfLocale language slug). There is no prefix/starts-with matching - add a PerfLocale language whose locale or slug matches exactly.

If your Polylang install used full locale slugs like en_gb, add a PerfLocale language with the matching slug exactly. The first import is refused, and writes nothing, while a Polylang language with content has no match or while Polylang’s default language is not PerfLocale’s default; wp perflocale migrate polylang --dry-run shows the check. Once an import has run, an unmatched language is reported and its content skipped.

Import before you translate in PerfLocale

Run the import before you start translating in PerfLocale. The import never replaces what you linked in PerfLocale: when a post already has a PerfLocale translation in a language its Polylang set also covers, or linking the set would move a post or term out of a translation group it shares with others, the whole Polylang set is left as it is and listed, for example “post 12 (fi, held by post 66)”. Remove or move the PerfLocale translation if Polylang’s is the one to keep, then run the import again.

When the languages differ. A post or term that PerfLocale already has in another language keeps PerfLocale’s language and is listed. A post or term alone in its group, whose language no earlier import set, takes Polylang’s language, and the import lists those whose earlier language was not the default one. To let the import set Polylang’s language where PerfLocale has another one, return 'source' from the perflocale/migration/language_conflict_policy filter in a plugin or must-use plugin, and remove it after the import. Each change is then listed, and the Polylang card on the settings screen, WP-CLI and the job’s row on the Jobs page say that the policy is on.

Run the import

From the admin

  1. Go to PerfLocale → Settings → Export & Import.
  2. Scroll to “Import from Polylang”. If the button isn’t shown, Polylang data wasn’t detected - the language taxonomy doesn’t exist and there are no language terms in the DB.
  3. Click Import from Polylang. Confirm the prompt.
  4. On completion the page reloads with a count of posts and terms imported. When items were left out, a yellow Needs attention notice lists each reason with examples; when part of the data could not be read or written, a red notice says so.

From WP-CLI

# Confirm Polylang data is detected.
wp perflocale migrate polylang --dry-run

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

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

When the import needs attention

The import finishes and lists what it left out, with up to five examples each, instead of reporting a plain success: a post or term Polylang lists that no longer exists, a language that has no PerfLocale language of its own (add it under PerfLocale → Languages and run the import again), a set left as it is because of a PerfLocale translation (see above), and a record of an earlier import that no longer matches its group. WP-CLI ends with Warning: instead of Success: and exit status 0, the admin shows a Needs attention notice, and a background import’s row on the Jobs page shows the same lines. A script that runs the import should check the exit status rather than look for Success:.

When the import cannot read or write part of the data, for example after a database error, it stops as incomplete: WP-CLI exits with an error (exit status 1), the admin shows the reason, and a background job ends as failed. What it imported is kept, and running it again imports the rest. String translations PerfLocale has no place for (such as widget texts), menu locations left as they are, the Price Sync and translatable-type messages and the language check’s findings are shown as notices and are not counted as errors.

Verification

  1. Post counts match. Compare the post-translation counts reported by the importer against your expected counts (Polylang’s admin → Languages tab shows totals per language).
  2. Spot-check a post. Open any multilingual post. The PerfLocale sidebar should list every language that had a translation in Polylang.
  3. Check terms. Confirm translated categories / tags are linked in the PerfLocale translation column.
  4. Visit translated URLs. Polylang’s URL prefix convention (e.g. /fr/) is matched by PerfLocale’s default slug-based prefix, so existing URLs should continue resolving. Browse a handful and confirm they render the right language.
  5. Check menus and strings. Switch languages on the front end and confirm each shows its own menu and site title. For theme and plugin strings, run wp perflocale strings scan (defaults to the active theme) and wp perflocale strings scan --dir=wp-content/plugins, then work through PerfLocale → Strings.

Re-running the import

The importer is idempotent. For each Polylang translation set, it reuses the PerfLocale group an earlier import recorded for it, when that group still holds only the set’s own posts or terms in the set’s languages, or a group that already contains one of them, and adds the missing members. Members that are already linked are skipped. If a run lists a language you hadn’t added yet, add it and re-run; the new language’s content fills in without duplicating the rest.

Rollback

No automatic rollback. The safe paths:

  • Restore from backup - the cleanest undo.
  • Reactivate Polylang - Polylang’s data is untouched, so reactivating restores its behaviour. PerfLocale’s newly-created translation groups will sit alongside Polylang’s without interfering (PerfLocale only acts when its own data is present and the plugin is active).

Troubleshooting

“Polylang language taxonomy not found”

Either Polylang was never set up on this site, or its data was deleted. Check for language rows in wp_term_taxonomy: SELECT COUNT(*) FROM wp_term_taxonomy WHERE taxonomy = 'language';. If the count is zero, there’s nothing to migrate.

The import timed out

Large migrations from the admin UI automatically queue via background jobs and complete in the background. If the queue path isn’t available on your host, switch to WP-CLI - admin-UI requests are bounded by PHP’s request timeout; WP-CLI isn’t.

Memory on very large sites

The Polylang importer fetches translation terms in batches rather than loading every post_translations / term_translations row in one query. It first lists the term_id column (cheap), chunks it by 100 by default, and only then reads the description payload (the serialised PHP array of lang_slug => post_id). Because maybe_unserialize expands every row into N PHP arrays in memory, this is the real lever for peak-memory tuning.

The filter is symmetric with the WPML one:

  • Tight memory budget: drop to 25 for more, smaller fetches.
  • Plenty of RAM, latency-sensitive: raise to 500 for fewer SQL roundtrips.
add_filter( 'perflocale/migration/polylang/batch_size', static fn(): int => 250 );

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

My translated strings didn’t come across

The importer brings over the site title, tagline and date and time formats. Widget texts and strings a theme or plugin registered with Polylang have no place in PerfLocale; the import result counts them. See “What’s not imported” above.

Next steps