Content Translation

Translate every piece of content on your WordPress site with precision and control.

Translate Any Content Type

Posts, pages, custom post types, categories, tags, and custom taxonomies. Each translation is a full WordPress post with its own URL, SEO metadata, and revisions.

PerfLocale dashboard with per-language progress bars showing percentage translated for posts, pages and products
Per-language progress, per post type — 100 % when every bar is green

Translation Status Tracking

Follow each translation through a clear pipeline from empty to published. Filter by status to see what still needs work at a glance.

Content Sync

Keep featured images, menu order, dates, and selected custom fields in sync across all language versions automatically. Edit once on the source post; every translation picks up the change.

What can be translated

PerfLocale ships with post and page as translatable post types, and category and post_tag as translatable taxonomies. PerfLocale → Settings → Translation lists every public post type and every public taxonomy registered on your site as a checkbox, so a custom type becomes translatable the moment you tick it. Developers can shape both lists from code instead, with the perflocale/translatable_post_types and perflocale/translatable_taxonomies filters.

Media is handled differently, and deliberately so: PerfLocale does not clone attachments. The attachment edit screen instead grows an Alt Text, Caption and Description field for every active non-default language. Posts that do not yet have a separate translation in every language also get a Featured Image per Language panel, so one post can show a different image per language without being duplicated at all.

How translations are linked

Every translatable object belongs to a translation group, and a link row ties one object to one language inside that group, carrying that pairing’s status and source. Two database-level unique keys keep the model honest: a language may appear only once inside a group, and an object may be linked to a given language only once for its type — so a page cannot quietly acquire a second German version. Because the linking lives outside wp_posts, each translation stays an ordinary WordPress post, with its own ID, editor, revisions and permalink. A post of a translatable type that still has no language is assigned the site default language automatically on save.

Creating a translation

Clicking + Create beside a language checks that you can edit the source post and then creates the translation — the classic-editor metabox redirects you straight into its editor, and the block editor offers an Open translation link the moment it lands. The content source is always the default-language post, even when you press Create from a French translation while adding German — a translation is never seeded from another translation. The new post’s status comes from the Default Translation Status setting: Empty, Draft or Pending Review. Empty and Draft both create a WordPress draft; “empty” is tracked on the link row rather than in post_status. For hierarchical types the translation is attached to its parent’s translation in the same language, but only when that parent really exists, is not in the trash, and is of the same post type — failing that, the child stays top level rather than inheriting a broken URL.

The create step then copies post meta, the featured image and taxonomy terms. Meta copying skips WordPress’s own bookkeeping keys and refuses outright to copy anything whose key looks like a credential — _password, _secret, _token, _api_key, stripe, paypal and friends. Both lists are filterable, through perflocale/translation/excluded_meta_keys and perflocale/translation/dangerous_meta_patterns. Each copy step is isolated, so one failure does not skip the others: whatever failed is recorded on the new post and surfaced in the editor as a one-shot notice naming the missing pieces.

Creates are serialised per source-and-target pair, so a double-click, a cron run and an editor save cannot between them produce two German copies of the same page. Auto-Create Stubs (off by default) does the same job on publish: the first time a default-language post goes live, an empty stub appears for every other active language.

Per-language status

Status is tracked per language on the link row, separately from WordPress’s own post_status. Five values exist, each with its own badge colour in the admin:

StatusBadgeMeaning
emptyGreyThe group has a slot for this language, but nothing has been written yet.
draftBlueA translator is working on it; the paired WordPress post is normally a draft.
pendingAmberSubmitted for review — shown as Pending Review — awaiting approval.
publishedGreenLive. Visitors in that language see this translation.
needs_updateRedThe source changed after this translation was last written.

PerfLocale → Translations renders your translatable content as that matrix: one row per source post, one column per language, one badge per cell linking through to the sibling’s edit screen. Filter by post type, language, status or source, search by title, then mark a selection Needs Update by hand — or, with a machine-translation provider configured, translate the selection in bulk. Every link row also records where its translation came from — manual, machine translation, a locally cached reuse, glossary, WP-CLI, webhook, the REST API, or a migration import from WPML, Polylang or TranslatePress — so you can pull up everything a machine wrote and review only those. More on the Translations matrix →

When the source changes

PerfLocale stores a SHA-256 hash of each source post’s title, content and excerpt, and recomputes it on every publish-status save. If the hash differs and the post that changed is the default-language member of its group, every other link in the group flips to needs_update in a single statement and the perflocale/content/changed action fires. Editing a translation never flags its siblings or its source, so a translator polishing the German copy does not tell the French translator to start over. Terms behave the same way, hashing name and description.

Fields that follow the source

Beyond featured image and menu order, the Sync Fields setting can also keep page parent, publish date, author, comment status, ping status and arbitrary meta keys aligned across a group. Keys you configure keep full mirror semantics — a save overwrites the siblings, a delete clears them — which is right for a page-builder layout that must stay structurally identical while its text is translated at render. Keys contributed by integrations, such as SEO titles or custom-field values, are seed-only: copied into a sibling that has none yet, then owned by that sibling’s translator and never overwritten again. The split is itself filterable, via perflocale/sync/mirror_meta_keys. Post parent is the one field that travels in a single direction, outward from the default language, and a post can be lifted out of the graph entirely with the Independent across languages checkbox on its editor screen.

Taxonomies and slugs

When a post translation is created, its terms come with it: for every translatable taxonomy the terms are sorted parents-first, each one’s translation is found or created, and the results are assigned to the new post — so the German article lands in the German category, not the English one. Keeping term parent structure identical across languages is on by default and maps each parent to that language’s own translated parent; turn it off when a market deliberately needs a flatter tree. Term names, slugs and descriptions always stay translator-owned.

Slugs need two mechanisms, because WordPress treats posts and terms differently. Posts may share a slug across languages: PerfLocale steps into WordPress’s uniqueness check and collapses the usual -2 suffix back to the intended slug when every conflicting post of that type is in another language — a genuine same-language collision keeps its suffix, because those two URLs really would be identical. Terms cannot share a slug at all, so a translated term is stored as base-de in wp_terms while its public display slug is recorded separately, and the URL uses the clean base. A trailing suffix is only ever stripped when it matches a real active language slug, so green-tea and sci-fi are left alone.

In the editor

In the block editor a Translations panel sits in the document sidebar: one row per active language with Create or Edit, the current language marked Current, and — on a post that has no language yet — a set of Set as buttons to declare which language it was written in. The classic editor gets the equivalent as a side metabox, with a language dropdown in place of those buttons. Both carry the same two per-post flags: Independent across languages, and Hide from hreflang and sitemap alternates for a translation you would rather search engines did not treat as an alternate.

Outside the editor, every translatable post type’s list table gains Language and Translations columns, an All Languages filter and a language field in Quick Edit; taxonomy screens get the same treatment. All of it is scriptable too, through the perflocale/v1 REST namespace, wp perflocale translations on the command line, and XLIFF export and import for agency workflows. Read the content-translation docs →

← Back to Features