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.

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 carries one of five statuses tracked separately from WordPress’s post_status. These are per-translation states — what the translator or editor sees in the Translations matrix:

  • Empty - placeholder; no content has been written.
  • Draft - content started but not ready.
  • Pending (label: Pending Review) - written but awaiting review before publication.
  • Published - ready; visitors see this translation.
  • Needs Update - the source post changed after this translation was last saved. Re-save the translation to clear.

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.

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.

← Back to Docs