---
title: "Content Translation - Documentation - PerfLocale"
description: "How PerfLocale stores and links translations for posts, pages, CPTs, and taxonomies. Covers default language, link groups, and status tracking."
canonical: "https://perflocale.com/docs/content-translation/"
source: "https://perflocale.com/docs/content-translation/"
format: "markdown"
---
# 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](https://perflocale.com/docs/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. Media attachments are a separate setting because translating alt text / captions is often desired without cloning the file itself.

## 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.

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.

## Navigation menus

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.

### Assigning a language to a menu

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 key | Stored on | Value |
| --- | --- | --- |
| _perflocale_language | The menu term | The language slug, or empty for Not set |
| _perflocale_menu_{slug} | Every menu in the group | Term 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_.

### Item pickers and language badges

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`](https://perflocale.com/docs/hooks/#perflocale-menu-badge-post-limit) filter.

### How it behaves on the front end

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 fix 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 does not apply there; PerfLocale localises those links at the block level instead, by filtering the rendered `core/navigation-link` and `core/navigation-submenu` output.

### Hooks

One filter is PerfLocale’s own — see the [hooks reference](https://perflocale.com/docs/hooks/#perflocale-menu-badge-post-limit) for its full entry; the rest are the WordPress hooks the menu integration attaches to, listed so you can judge ordering against your theme.

| Hook | Kind | What PerfLocale does |
| --- | --- | --- |
| perflocale/menu/badge_post_limit | Filter (PerfLocale) | Row cap for the language-badge lookup. int, default 5000; 0 loads every row. |
| wp_nav_menu_args | Filter | Swaps a theme location or numeric menu ID to that group’s menu for the request language. |
| wp_nav_menu_objects | Filter (priority 9) | Re-points post and term items at the current language’s counterpart, in menus with no language assigned. |
| wp_nav_menu_objects | Filter (priority 10) | Rewrites front-page items to the per-language home URL. |
| wp_update_nav_menu | Action | Saves the menu language and propagates the linked-menu pointers. |
| wp_create_nav_menu | Action | Same, for a menu created rather than updated. |
| load-nav-menus.php | Action | Fallback save, for the case where the menu ID changes mid-request. |
| admin_enqueue_scripts | Action (priority 20) | Adds the Menu Language field and the badge script on nav-menus.php. |
| admin_head-nav-menus.php | Action | Installs the get_pages and pre_get_posts picker filters. |

## Related

-   [Language Fallbacks](https://perflocale.com/docs/language-fallbacks/) - what happens when a translation is missing.
-   [Machine Translation](https://perflocale.com/docs/machine-translation/) - auto-translate new posts.
-   [Permissions & Roles](https://perflocale.com/docs/permissions/) - the Translator role and its capabilities.
-   [Shortcodes](https://perflocale.com/docs/shortcodes/) - the `[perflocale_switcher]` and related.

[← Back to Docs](https://perflocale.com/docs/)
