Content Translation

PerfLocale treats every translatable object (post, page, custom post type, taxonomy term) as a member of a translation group. Each group has one entry per active language; the default language’s entry is the source, every other entry is a translation. The source post is a regular WordPress post - translations are separate WP posts linked to it through an internal join table.

Default language

The default language is the one your WordPress site was originally written in - English, German, whatever. Set it at PerfLocale → Languages (one language row carries a DEFAULT badge). The default language is the canonical source for every piece of content; translations into other languages are derived from it. Changing the default after content exists is possible but requires care - see Migration.

Translatable post types & taxonomies

Out of the box, post and page are translatable, with category and post_tag as the default taxonomies. Enable custom post types and additional public taxonomies at PerfLocale → Settings → Translation - every public type appears there as a checkbox, and integration addons (e.g. WooCommerce) enable their own types automatically. A collapsed Advanced content types group below them offers the few non-public types PerfLocale can translate: synced patterns and block-theme menus. Both are off by default. Full Site Editing is its own row in the same section, also off by default - switch it on and the block templates and template parts you edit in the Site Editor (header, footer, 404, and the rest) become translatable too; see Full Site Editing.

Media is not in that list on purpose. PerfLocale does not translate an image by cloning the attachment - it keeps one file and stores the translated alt text and caption as language-suffixed meta on that same attachment, so every language shares the upload and only the words differ.

Creating a translation

  1. Open the source post in the editor.
  2. In the PerfLocale Translations panel (Gutenberg sidebar or the classic-editor metabox), click + Create next to the target language. A new post is created in that language, linked to the source; once it exists the button becomes Edit.
  3. Edit the translated post independently - it’s a normal WP post in every respect, just tied to its source through the translation group.

Who can create a translation. A new translation is copied from the group’s default-language original when it has one, even when you press + Create on another translation, so the button checks everything that copy involves: the perflocale_translate capability, edit rights on the post you started from, edit and read rights on that original, and WordPress’s own permission to create that kind of content - for a synced pattern, publish_posts. A refusal names the reason, for example You cannot edit the original this translation is copied from. In the classic-editor metabox, + Create is shown greyed out to an account without perflocale_translate or without that create permission; the block editor shows the reason as a notice when you click it. See Permissions & Roles.

Password-protected posts. A new translation of a password-protected post gets the same password. When the password of the default-language post is set, changed or removed later, each translation whose password matched the previous one gets the new one; only the password and the modified date change, and the translation’s content, title, excerpt, publish date and status stay as they are. A translation with a password of its own, and a private translation, keep theirs, and a save copies the password only to the translations the person saving may edit, as for Sync Fields. Machine translation sends a password-protected post by default only when someone picks that post; the automatic runs and wp perflocale translate --all skip it (see Machine Translation). ACF and Pods password fields, and Meta Box password fields stored in post meta, inside groups too, are not copied into a new translation; a Meta Box group that content sync later copies into an existing translation leaves its password sub-fields behind too.

Alternatively, create a post directly at Posts → Add New and assign its language in the PerfLocale Translations panel - the Set as buttons in the Gutenberg sidebar, or the Language dropdown in the classic-editor metabox. To place a post in an existing translation group, use the source post’s + Create button; two already-existing posts cannot be linked manually from the sidebar.

Translation statuses

Each translation shows one of five statuses - what the translator or editor sees in the Translations matrix. Four of them come from the translation’s own post; Needs Update is a flag PerfLocale keeps:

  • Empty - no translation in that language yet, or the translation is in the Trash.
  • Draft - the translation is a draft.
  • Pending (label: Pending Review) - the translation is pending review before publication.
  • Published - the translation is published; visitors see it.
  • Needs Update - the source post changed after this translation was last saved (re-save the translation to clear), or the translation’s Sync Fields differ from its group’s.

The exact rules are under How a status is decided.

Sync Fields

Some values belong to the whole translation group rather than to one language. Choose them at PerfLocale → Settings → Translation, under Sync Fields: Featured Image, Menu Order, Page Parent, Publish Date, Author, Comment Status and Ping Status, plus any meta keys you list under Custom Meta Keys, one per line. Featured Image and Menu Order are selected by default. When a post is saved, PerfLocale copies these fields to the other translations in its group; a translation that already holds the same value is not written, and only the synced fields and its modified date change.

Most fields are copied from whichever translation is saved, so changing the menu order on the German page changes it on the English one too. Three kinds travel one way only, from the default-language post (the original) to its translations: Page Parent (each translation gets the parent’s translation in its own language), Publish Date, and the values add-ons copy once into a translation that has none yet, such as SEO titles and custom-field values. The meta keys you list are copied in both directions, so list only values that hold no words, such as a flag or a number. A post with Independent across languages ticked in its Translations panel neither sends nor receives Sync Fields. PerfLocale’s own internal fields (meta keys that start with _perflocale_) cannot be listed, apart from the few PerfLocale allows there, such as the hreflang and sitemap exclusion: the Settings screen leaves them out and names them, for example “Not added to Sync Fields: _perflocale_meta_mt_errors. PerfLocale keeps these fields per post and never syncs them between translations.”

A new translation

A new translation starts with the original’s Menu Order, Comment Status, Ping Status and Publish Date when those are Sync Fields, so its first save changes nothing on the other translations. With Publish Date, the translation is still created as a draft (or as pending, when that is the Default Translation Status), and the date alone neither schedules nor publishes it. When the original is scheduled, publishing the translation schedules it for the same time. If the original is a draft without a set date, the translation gets a date when it is saved, like any draft.

Who can change the other translations

A save copies Sync Fields only to the translations the person saving may edit. Translations they cannot edit keep their own values.

  • Administrators and editors can edit every translation, so their saves update all translations. PerfLocale’s Translator role can edit all posts and pages too.
  • Authors and contributors usually own only their own translation. Their saves update that translation and any other translation they may edit, but not the original or translations owned by others.

When a save skipped a translation that would have received a different value, PerfLocale shows a warning such as Sync Fields were not copied to 2 translations that you cannot edit. In the block editor it appears after saving; on the classic edit screen, after the page reloads. Translations that already hold the same values are not counted, so no warning appears when nothing would have changed. A changed post password is copied by the same rule (see Password-protected posts).

When a save cannot reach every translation

A translation that a save skipped is marked Needs Update when the group’s original holds the saved values: the saved post is the original, or the person saving may edit it. When the save could not reach the original either, the original keeps its values and the skipped translations are left unmarked; the saved translation is marked instead, if the save would have changed the original. In a group without a default-language post, the saved post’s values count as the group’s.

A translation marked this way reads Needs Update on the Translations screen, and its own saves copy none of its Sync Fields to the group: not from a scheduled task, WP-CLI, an import or a background job, and not from Quick Edit or Bulk Edit. The mark is cleared when a person who may edit both that translation and the group’s original saves it from its edit screen. That save copies to the group only the Sync Fields the person changed; for every other Sync Field the translation takes the group’s current values from the original. In the block editor, a value that a box below the editor (custom fields, another plugin’s meta box) still shows from before that save does not count as a change until the page is reloaded. If a change to the original’s text marked the translation Needs Update in the meantime, that status stays until the translation’s text is updated.

To bring every translation back in line at once, someone who may edit all of them, such as an editor, saves the original. Its Sync Fields are then copied to every translation.

Bulk Edit and Quick Edit

Bulk Edit and Quick Edit follow the same rule. After a Bulk Edit, the posts list shows the same warning with the number of translations that were not updated. Quick Edit saves without reloading the page and shows no warning; translations the user cannot edit keep their values all the same.

Scheduled posts

When WordPress publishes a scheduled post on its own, the copy follows the rights of the post’s author (the user shown as its author), whoever scheduled it, as if the author had saved it. A post whose author has the Author role does not change the original or the translations that author cannot edit. A post whose author is an editor or administrator updates all translations when it is published. A contributor’s post that an editor scheduled also updates all translations: contributors cannot edit published posts, so the publish counts as the site’s own. To name another user, or none, use the perflocale/sync/scheduled_publish_user filter.

Saves without a logged-in user

Other saves made with no logged-in user update all translations: WP-CLI commands run without --user, imports run from WP-CLI or in the background, and background tasks such as machine translation jobs. A WP-CLI command run with --user=<user> follows that user’s rights. A save by a user who may not edit the saved post, such as a checkout updating a product, is the site’s own and updates all translations too. A translation marked Needs Update by Sync Fields copies nothing in any of these cases.

The Translations matrix

Visit PerfLocale → Translations for a cross-language overview of every post. Rows are source posts; columns are languages. Each cell shows the translation’s status as a coloured badge with a shortcut arrow to edit. Filter by post type, language, or status; search by title.

Site title and tagline

The site title and tagline are options rather than posts or theme strings, so the content pipeline and the string scanner do not reach them. They are listed on the Strings screen as Site Title and Tagline, and translated there like any other string. Nothing needs enabling and no scan is required - they are registered when the plugin is activated or updated.

Your stored title and tagline are never modified. The translation is served on the front end only, and PerfLocale refuses any write that would put a translated value back over the original - including from wp-admin, WP-CLI, XML-RPC and the REST API, all of which read and re-save options in ways that would otherwise overwrite the source.

Blocks and patterns

Ordinary block content needs no special handling: a post’s blocks are part of its post_content, so translating the post translates its blocks. Patterns are the case that surprises people, because “pattern” means two different things and they behave in opposite ways.

Patterns you insert

When you pick a pattern from the block inserter, WordPress copies its markup into the page. From that moment the text belongs to the page and has no connection to the pattern it came from. So you translate it by translating the page, under PerfLocale → Translations - the text comes with it, like any other content.

This is also why the theme’s pattern strings on the Strings screen do not help here. A theme pattern file such as patterns/cta-book-links.php may call esc_html_e(), which puts the string on the Strings screen - but that string is only used when the theme renders the pattern file itself, for instance from a template. The copy sitting in your page is plain HTML and nothing on the Strings screen can reach it. Nothing is broken; they are simply two different copies of the same words.

Synced patterns

A synced pattern is the opposite. It is stored once as a wp_block post, and every page that uses it holds only a reference - <!-- wp:block {"ref":123} /-->. There is no text in the page to translate, which is why translating the page has no effect on it.

Enable Patterns under Advanced content types at PerfLocale → Settings → Translation. Each synced pattern then becomes translatable like any other post, and on the front end PerfLocale points the reference at the translation for the language being viewed. Patterns nested inside a Group or Columns are handled the same way.

To create a pattern’s translation, open the pattern in the Site Editor (Appearance → Editor → Patterns), open the Translations panel in the settings sidebar and press + Create next to the language. The translation is a copy of the pattern, created as a draft (or as pending, when that is your Default Translation Status); its row then shows Draft and an Edit link that opens it in the Site Editor. The copy keeps the original’s name, so the panel reminds you to rename it in its language: the Site Editor’s pattern lists then tell the two apart.

Patterns in translated templates. When a pattern has a translation, the templates and template parts of that language, such as Header (de), use it, by the same rules as menus (see Translating menus in block themes): a pattern you chose yourself stays, and deleting or trashing the translation brings the original back. Edit original in Header (de) then opens the German pattern.

Two deliberate behaviours are worth knowing. If a pattern has no translation for the current language, or its translation is still a draft, the original renders instead - WordPress renders nothing at all for a reference it will not accept, and a blank space where a pattern should be is worse than untranslated text. And per-instance overrides are stored in the page rather than in the pattern, so they survive translation untouched.

The same fallback covers a translation that is published but empty (or holds only whitespace). WordPress accepts one and renders a blank, and an empty draft published unedited is an easy way to get one, so the original renders instead. As a result, publishing an empty translation does not hide a pattern in that language; if you want a pattern to show nothing in one language, give its translation a block that displays nothing.

Creating a pattern translation takes WordPress’s own permission to create patterns, which is publish_posts. The bundled Translator role does not have it, so a Translator can edit a pattern translation that already exists but cannot create one.

Navigation menus are not translated the way posts are. A menu is a nav_menu term, and PerfLocale deliberately never auto-assigns a language to nav_menu terms or puts them in a translation group. Instead you build one menu per language in the ordinary WordPress menu editor, tag each one with a language, and tell PerfLocale which menus belong together. On the front end PerfLocale then swaps in the right menu for the language being viewed.

Go to Appearance → Menus. PerfLocale appends a Menu Language fieldset to the end of the Menu Settings box, below the usual Auto add pages and Display location controls. It contains one dropdown, perflocale_menu_language, listing Not set followed by every active language - each shown with its flag, its native name (its English name when no native name is stored) and its BCP-47 code.

Pick a language and press Save Menu. The choice is stored as term meta on the menu itself. Saving requires the edit_theme_options capability and WordPress’s own nav-menu nonce (field update-nav-menu-nonce, action update-nav_menu); a programmatic menu save from WP-CLI or an importer carries neither, so it never touches the language assignment.

Linking menus across languages

Once a menu has a saved language, reloading the screen adds a Linked Menus block to the same fieldset: one dropdown per other active language, each listing every menu on the site with - None - as the default. Choose the counterpart menu for each language and save.

You only have to do this once, from one menu in the set. When PerfLocale saves the links it builds the whole group - the menu you are editing plus every menu you pointed at - and then writes pointers between every pair in that group, in both directions. Fill in the links on your English menu and the German and Polish menus come back pointing at each other and at English too.

Term meta keyStored onValue
_perflocale_languageThe menu termThe language slug, or empty for Not set
_perflocale_menu_{slug}Every menu in the groupTerm ID of that group’s menu for language {slug}

Renaming a language slug on PerfLocale → Languages rewrites both of these - the _perflocale_menu_ key suffixes and the _perflocale_language values - so an existing menu group survives the rename instead of quietly falling back to None.

While you are editing a menu that has an explicit language, PerfLocale narrows the post-type panels in the “Add menu items” column so they only offer content in that language. Pages are filtered through get_pages; posts and custom post types through a pre_get_posts constraint scoped to the nav-menus screen. Taxonomy panels - Categories, Tags and any other term picker - are left alone and still list every term.

The post-type picker filter is strict. Only content explicitly linked to the menu’s language is offered. Posts and pages that PerfLocale is not managing at all - content that predates the plugin and has never been given a language - are excluded rather than shown as a fallback, so a menu for one language cannot silently collect unassigned content. If a page you expect is missing from the picker, give it a language in the editor’s Translations panel first.

A menu left on Not set is never filtered: its pickers show everything, exactly as they do without PerfLocale.

Separately, whenever two or more languages are active, PerfLocale adds a small language-code badge to the item labels in that column so you can tell at a glance which language each candidate belongs to. The badge data covers every published post, page or custom-post-type item PerfLocale has linked to a language, newest-modified first, capped by the perflocale/menu/badge_post_limit filter.

For a theme location, PerfLocale looks up the menu the theme has assigned to that location and, if the request is in another language, substitutes that menu’s counterpart before WordPress renders it. The same substitution happens when a template passes a numeric menu ID directly. A menu passed by name or slug is left alone.

Resolution works like this: if the assigned menu is already tagged with the request language it is used as-is; otherwise PerfLocale follows that menu’s _perflocale_menu_{slug} pointer. A pointer is only honoured if the target menu still exists and has not since been re-tagged to a different language - a target left on Not set is still trusted, since the usual pattern is to tag one menu in the group and simply link the rest. If nothing resolves, the theme’s original menu renders unchanged. That is the fallback the field’s own help text describes: If no menu is linked for a language, the default menu will be shown.

Items inside a menu that has no language assigned are handled individually instead. A classic menu item stores the object it points at, and its permalink resolves to that object’s own language - so on a translated page a shared menu would link every visitor back to the source language. PerfLocale re-points post-type and taxonomy items at the current language’s counterpart where one exists. Custom links and archive items are never rewritten, and a post whose counterpart is not published is skipped so the item keeps a URL that works.

Menus that do carry a language assignment are left untouched at item level. They were curated for that language on purpose, and the menu swap above has already picked the right one.

One further rule applies to every menu: an item pointing at the static front page (or at any page in the front page’s translation group) is rewritten to the home URL of the language that page belongs to - the default language when the page carries none - rather than to the page’s own permalink, so a German front-page item lands on /de/ and not /de/homepage/. The rewrite goes through PerfLocale’s URL converter, so it is correct in every URL mode - subdirectory, subdomain, domain per language and query parameter alike.

Classic menus only. Everything in this section concerns menus rendered by wp_nav_menu(). Block themes render the Navigation block instead, which never calls wp_nav_menu(), so the per-language menu swap above does not apply there. Block-theme menus are handled separately: link addresses are localised at the block level with no setting, by filtering the rendered core/navigation-link and core/navigation-submenu output, and the visible labels can be translated by enabling Navigation Menus under Advanced content types.

One filter is PerfLocale’s own - see the hooks reference for its full entry; the rest are the WordPress hooks the menu integration attaches to, listed so you can judge ordering against your theme.

HookKindWhat PerfLocale does
perflocale/menu/badge_post_limitFilter (PerfLocale)Row cap for the language-badge lookup. int, default 5000; 0 loads every row.
wp_nav_menu_argsFilterSwaps a theme location or numeric menu ID to that group’s menu for the request language.
wp_nav_menu_objectsFilter (priority 9)Re-points post and term items at the current language’s counterpart, in menus with no language assigned.
wp_nav_menu_objectsFilter (priority 10)Rewrites front-page items to the per-language home URL.
wp_update_nav_menuActionSaves the menu language and propagates the linked-menu pointers.
wp_create_nav_menuActionSame, for a menu created rather than updated.
load-nav-menus.phpActionFallback save, for the case where the menu ID changes mid-request.
admin_enqueue_scriptsAction (priority 20)Adds the Menu Language field and the badge script on nav-menus.php.
admin_head-nav-menus.phpActionInstalls the get_pages and pre_get_posts picker filters.

Block themes: the Navigation block

Everything above describes classic nav_menu menus. Block themes do not use them - a menu there is a wp_navigation post, referenced by the Navigation block in the same way a page references a synced pattern, so the language field at Appearance → Menus does not apply.

Link addresses are already translated without any setting: a menu item pointing at a post resolves to that post’s translation for the language being viewed. What is not translated by default is the visible label, because the label is stored in the menu post rather than derived from the target.

To translate labels, enable Navigation Menus under Advanced content types at PerfLocale → Settings → Translation, then translate the menu the way you would any post. On the front end the Navigation block is pointed at the menu for the current language. As with patterns, a menu whose translation is missing, unpublished or empty falls back to the original rather than rendering an empty menu.

A Navigation block with no menu chosen, as in the headers of the default block themes, shows WordPress’s fallback menu. With Navigation Menus enabled, that fallback is the newest published menu in the default language (or without a language) on default-language pages, and that menu’s published translation on translated pages. A header saved with a chosen menu keeps that menu until you choose another.

Translating menus in block themes

  1. Enable Navigation Menus under Advanced content types at PerfLocale → Settings → Translation.
  2. Open Appearance → Editor → Navigation, choose the menu, and open it for editing. The Translations panel in the settings sidebar lists every active language.
  3. Press + Create next to a language. The translation is a copy of the menu, created as a draft (or as pending, when that is your Default Translation Status); the canvas keeps showing the menu you had open.
  4. Follow the row’s Edit link, translate the labels in the canvas, and press Publish. You can also publish a translation from the original menu’s Translations panel: a draft or pending row offers Publish. A draft is not used, and the original renders instead.

Rename each translation in its language. A new menu translation keeps the original’s name, and the panel reminds you to rename it after + Create: unless the Navigation block has a label of its own, WordPress uses the menu’s name as the label screen readers announce for the navigation, so a German menu named “Main menu” is read out in English. In the Site Editor’s Navigation list, each menu’s language follows its name, for example Main menu (EN).

Translated templates and template parts use the menu of their language. When a language has a translation of a menu, that language’s templates and template parts, such as Header (de) and its Navigation Overlay part, use the translated menu: PerfLocale points them at it when the template’s translation or the menu’s translation is created or linked, for example by an import, whichever comes second. A restore from a PerfLocale export does the same, and a menu that moves to another language is no longer used by the templates of the language it left. PerfLocale changes the stored template or part directly, which makes no revision. Editing the menu inside Header (de) in the Site Editor then edits the German menu only. A template or part in which you chose another menu keeps your choice, also when you switched it back to the original menu on purpose. When you delete or trash a menu translation, that language’s templates and parts use the original menu again; restoring it from the Trash points the templates and parts that used it back at it, including those where you had picked it yourself. Templates and parts that the person who creates or links the translation may not edit are left as they are; wp perflocale template-refs-backfill updates them.

When many templates and parts of a language use the menu or pattern, the request that made the change updates the first of them, and a background job with its own row on PerfLocale → Jobs updates the rest.

Templates and parts translated earlier. Once per site, a background job points the menus and synced patterns of the translated templates and parts that still use the original-language ones at their language’s translations, by the same rules. It starts with the first admin page view of a user who may edit the theme, runs in the background, and its row on PerfLocale → Jobs counts the templates and parts it read and updated and the references it pointed at a translation. What visitors see does not change: those pages already show the translations. wp perflocale template-refs-backfill runs it again, for example after you linked translations someone without access to the templates had created (see WP-CLI).

Shared menus and patterns are marked. In a translated template or template part, select a Navigation block or a synced pattern that is still the original-language one: its block settings say This menu is shared with English. Edit its Deutsch translation instead. (or This pattern is shared with English. …), with Open for an existing translation or + Create for a new one. The block then uses the new translation when it is created as a draft or published. Editing a shared menu or pattern there changes it in every language that shows it.

A translation is a copy, made when you create it. Items you later add to, remove from or reorder in the original menu do not reach its translations. When the published original changes, its translations are marked Needs Update on the Translations screen; the Site Editor’s panel shows only whether each translation is published. Make the same change in each translation.

On WordPress 7.0 or newer, a Navigation block’s overlay (the menu that opens on small screens) is translated too: the overlay shows the translation of its template part, and of a menu, synced pattern or template part placed directly in it.

← Back to Docs