---
title: "Content Translation - PerfLocale"
description: "Translate posts, pages, custom post types, taxonomies, and URL slugs with translation status tracking."
canonical: "https://perflocale.com/features/content-translation/"
source: "https://perflocale.com/features/content-translation/"
format: "markdown"
---
# 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](https://perflocale.com/assets/shots/01-dashboard--top.png)

One bar per language per post type — tracks done, draft, outdated and missing. Fully green when nothing’s left to translate. 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`](https://perflocale.com/docs/hooks/#perflocale-translatable-post-types) and [`perflocale/translatable_taxonomies`](https://perflocale.com/docs/hooks/#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`](https://perflocale.com/docs/hooks/#perflocale-translation-excluded-meta-keys) and [`perflocale/translation/dangerous_meta_patterns`](https://perflocale.com/docs/hooks/#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:

| Status | Badge | Meaning |
| --- | --- | --- |
| empty | Grey | The group has a slot for this language, but nothing has been written yet. |
| draft | Blue | A translator is working on it; the paired WordPress post is normally a draft. |
| pending | Amber | Submitted for review — shown as Pending Review — awaiting approval. |
| published | Green | Live. Visitors in that language see this translation. |
| needs_update | Red | The 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](https://perflocale.com/docs/migration/) from WPML, Polylang or TranslatePress — so you can pull up everything a machine wrote and review only those. [More on the Translations matrix →](https://perflocale.com/docs/translations/)

## 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`](https://perflocale.com/docs/hooks/#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`](https://perflocale.com/docs/hooks/#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 →](https://perflocale.com/docs/content-translation/)

[← Back to Features](https://perflocale.com/features/)
