Permissions & Roles

PerfLocale provides a comprehensive permissions system with a custom Translator role and granular capabilities that integrate with WordPress's built-in role system.

Custom Role: Translator

Slug: perflocale_translator

The Translator role is designed for team members who only need to translate content. They can edit posts and pages (to translate them) but cannot publish, delete, or manage site settings.

WordPress capabilities included:

CapabilityGranted
readYes
edit_postsYes
edit_others_postsYes
edit_published_postsYes
edit_pagesYes
edit_others_pagesYes
edit_published_pagesYes
upload_filesYes
publish_postsNo
delete_postsNo
manage_optionsNo

Creating a translation also takes edit and read rights on the original it is copied from, and the content type’s own create capability (see Creating a translation). Without publish_posts, a Translator therefore cannot create translations of patterns (the wp_block type, synced or not), because WordPress requires publish_posts to create a pattern. They can still edit pattern translations that already exist.

Custom Capabilities

PerfLocale adds 7 custom capabilities that control access to specific plugin features. The canonical list is TranslatorRole::CAPABILITIES.

Capability Matrix

CapabilityTranslatorEditorAdministratorDescription
perflocale_translateYesYesYesCreate and edit translations (creating has further checks)
perflocale_manage_translationsNoYesYesSupervisor cap: Strings screen, bulk machine translation, other users’ background jobs
perflocale_approve_translationsNoYesYesReserved for approval integrations (no core screen consumes it yet)
perflocale_manage_languagesNoNoYesAdd, edit, delete languages
perflocale_manage_addonsNoNoYesManage plugin addons
perflocale_use_mtYesYesYesUse machine translation (incl. bulk jobs)
perflocale_import_exportNoNoYesImport/export translations (XLIFF, PO/MO, JSON)

Capability Details

perflocale_translate

The core translation capability. Users with this cap can:

  • Create translation versions of posts, pages, and terms, subject to the checks under Creating a translation
  • Edit existing translations
  • Use the Translations panel in the block-editor sidebar
  • Use + Create in the classic-editor metabox, the panels on host screens such as the WPForms form builder and Contact Form 7, and the term edit screen. Those list the translations to anyone who can edit the post or term, but offer + Create only with this capability

perflocale_manage_translations

Supervisor-level access:

  • View and edit the Strings admin screen
  • Dispatch bulk machine translation from the Translations page and the POST /translations/bulk-translate endpoint
  • Cancel, retry, delete and read the full args of background jobs dispatched by other users

perflocale_approve_translations

A reserved sign-off capability for approval-style integrations. No core screen or route consumes it today — it exists so an addon or custom code can gate an approve/reject step (a translation submitted as a suggestion, published only after sign-off) without granting reviewers the broader perflocale_manage_translations access. Granted to Editors and Administrators but not the base Translator role. Note that publishing a translation post itself is governed by WordPress’s native publish_posts capability, which the Translator role deliberately lacks.

Because it is a separate capability, you can grant it on its own to a dedicated reviewer role without also handing over the broader perflocale_manage_translations access.

perflocale_manage_languages

Administrative control over languages:

  • Add new languages
  • Edit language settings (name, locale, flag, direction)
  • Delete languages
  • Set the default language
  • Reorder languages

perflocale_manage_addons

Addon configuration on the WP Admin → PerfLocale → Addons page. Holders of this cap can:

  • View the Addons page (addon cards, status, version-mismatch notices)
  • Save an addon’s settings form on its subtab under Settings → Addons (reached from the card’s Manage → link) — persists to the autoloaded perflocale_addon_settings option keyed by addon ID
  • Click the per-card Enable / Disable button — writes to perflocale_disabled_addons and flushes the bootable-addons transient so the change takes effect on the very next request. Works for both bundled (in-plugin) and external addons

Addons themselves still auto-activate based on host plugin / theme detection — this cap controls who can configure them, not whether they load.

perflocale_use_mt

Machine translation access:

  • Trigger machine translation for individual posts
  • Access the "MT" button in the editor sidebar
  • Start, monitor, and cancel bulk translation jobs (entire post types)
  • Uses the configured MT provider (DeepL, Google, Microsoft, LibreTranslate, an external agency, or the WordPress AI Client on WP 7.0+)

perflocale_import_export

Data portability:

  • Export translations to XLIFF 2.0 format
  • Import XLIFF files
  • Export string translations to PO/MO files
  • Import string translations from a PO file on the Strings screen
  • Run the full-site JSON export / import under Settings → Export & Import

Migrating from WPML, Polylang, or TranslatePress is gated separately on manage_options.

What a PO upload on PerfLocale → Strings or a JSON import under Settings → Export & Import stores also depends on WordPress’s unfiltered_html capability. For an account without it, imported string translations, languages and translated slugs are cleaned with the rules PerfLocale applies when it saves the same data itself. Accounts with it, and WP-CLI (wp perflocale import, network-import and po-import), store those values exactly as the file has them. See String Translation and Export & Import.

Creating a translation

Each way of creating a translation has its own capability, listed in the tables below: perflocale_translate for + Create, perflocale_use_mt for machine translation, perflocale_import_export for XLIFF import. On the paths listed below, a request that would create a new translation must also pass the checks in this section; each check names the paths it applies to. They run before the translation is created and before any text is sent to a machine-translation provider. They do not apply to opening or editing a translation that already exists.

Edit and read rights on the original

PerfLocale copies a new translation from the post in the site’s default language — the original of the translation group, when it has one — which is not always the post you start on. You must be able to edit and read that original as well, or the request is refused with “You cannot edit the original this translation is copied from.”

The check applies only when the original is a different post from the one you started on; starting on the original itself is covered by the ordinary edit check. It is applied on these paths:

  • the + Create button in the block-editor sidebar, and the REST route behind it, POST /translations/post/{id} (error code source_forbidden, HTTP 403). The sidebar shows the refusal as an error notice;
  • the + Create link in the Translations panel (the classic-editor metabox and the panels on host screens) and on the term edit screen. For a term, the original is the default-language term and the check is edit_term;
  • XLIFF import: the unit is skipped with the message in the import’s error list, and the rest of the file is still imported;
  • machine translation through POST /machine-translate and POST /machine-translate/object (posts and terms), checked before the request counts against the rate limit;
  • the AI-agent abilities perflocale/create-translation and perflocale/translate-post;
  • bulk machine translation (the bulk_translate and site_translate jobs): the row is skipped and counted as skipped, and the job reports the message if it is its first error.

The content type’s create capability

A translation is a new post of the original’s type, so PerfLocale also asks for the capability the type registers for creating posts (its create_posts), the one WordPress checks before it shows that type’s Add New screen. For posts that is edit_posts and for pages edit_pages, which every built-in role that can edit posts or pages already has, as does PerfLocale’s Translator role. Some types ask for more: a pattern (the wp_block type, synced or not) needs publish_posts, so a user who can edit patterns but not publish posts cannot create a pattern translation. The refusal reads “You do not have permission to create translations of this content.” (REST error code cannot_create_type, HTTP 403).

This check applies to posts; terms have no equivalent. It is made by + Create (button and link), POST /translations/post/{id}, XLIFF import, the two /machine-translate routes and both AI-agent abilities.

Content from plugins that manage their own permissions

Both checks ask the plugin that owns the content type when WordPress cannot answer, as the edit check of the REST routes and the + Create link does. Contact Form 7 maps its own capabilities (to publish_pages by default), so WordPress answers for its forms. WPForms registers capabilities that no role holds, so for its forms the bundled WPForms add-on answers with WPForms’ own permissions: create_forms to create a translation, edit_form_single to edit or read a form. Another plugin in the same position can answer through the perflocale/object/user_can filter, which is also asked with the actions read and create.

A content type whose create capability has been switched off entirely (Pods’ Disable Add New forms option does this) cannot have translations created through these paths by anyone, multisite super admins included.

The + Create link in the Translations panel and on the term edit screen goes through an admin action. That action requires perflocale_translate and uses the same edit check as the REST route, which is what lets + Create work in the WPForms form builder.

Where PerfLocale can tell in advance that the action would refuse you, it shows + Create as plain text instead of a link (greyed out in the Translations panel), with the tooltip “You do not have permission to create translations of this content.” It does this on a post when you lack perflocale_translate or the type’s create capability, and on a term when you lack perflocale_translate. The check on the original runs only when you follow the link, so a live link can still be refused with “You cannot edit the original this translation is copied from.”

If the translation already exists, for example because it was created in another tab, the action opens it. It does not create it again or machine-translate over it. With Auto-Translate on Create on, the machine-translation step has conditions of its own, listed under Machine Translation → Auto-translate on Create.

Where these checks do not apply

WP-CLI commands such as wp perflocale translations create and wp perflocale translate do not check WordPress capabilities, so none of these checks apply to them. Nor do they apply to the automatic flows switched on in Settings, Auto-Create Stubs and Auto-Translate on Publish, which start from the default-language post itself.

Admin Menu Visibility

Menu ItemRequired CapabilityNotes
Dashboardperflocale_translate
Languagesperflocale_manage_languages
Stringsperflocale_manage_translations
Translationsperflocale_translateFilterable content list with per-language status
Addonsperflocale_manage_addons
Jobsperflocale_translateBackground-jobs queue; per-row mutation rules below
Settingsmanage_options (WP core)

REST API Permissions

EndpointRequired Check
GET /languagesPublic by default (no auth required) - gateable via perflocale/api/languages_public filter
POST /languagesperflocale_manage_languages
PUT/DELETE /languages/{slug}perflocale_manage_languages
POST /languages/reorderperflocale_manage_languages
GET /translations/{type}/{id}perflocale_translate + edit_post
POST /translations/{type}/{id}perflocale_translate + edit_post, plus the creation checks on the original and the content type
POST /translations/post/{id}/languageperflocale_translate + edit_post
PUT /translations/{type}/{id}/{lang}perflocale_translate + edit_post
DELETE /translations/{type}/{id}/{lang}perflocale_translate + delete_post
POST /translations/bulk-translateperflocale_manage_translations
GET /stringsperflocale_translate
POST /strings/scanmanage_options
POST /strings/machine-translateperflocale_use_mt
POST /machine-translate, POST /machine-translate/estimate, POST /machine-translate/objectperflocale_use_mt + per-object edit_post / read_post, plus the creation checks when a translation would be created
POST /block-translate, POST /block-translate/from-sourceperflocale_use_mt + edit_post
POST /xliff/export, POST /xliff/importperflocale_import_export (export additionally checks edit_post per requested post; import checks edit_post per referenced post, plus the creation checks for each translation it creates)
POST /webhooksmanage_network_options on multisite, manage_options on single site — filterable via perflocale/webhooks/register_capability
GET /webhooks, DELETE /webhooks/{id}manage_options
GET /configPublic, but only registered when the edge_integration_enabled setting is on
GET /jobs, GET /jobs/{id}perflocale_translate (args redacted for non-creator non-supervisor users)
POST /jobs/{id}/cancel, POST /jobs/{id}/retry, DELETE /jobs/{id}perflocale_translate + (creator OR perflocale_manage_translations)

WordPress Abilities API Permissions

When the Abilities API integration is enabled (via the perflocale/abilities/enabled filter on WP 6.9+), each registered ability ships its own permission_callback. AI tools and external consumers must satisfy these checks per call.

AbilityRequired Check
perflocale/list-languagesPublic
perflocale/get-translationsperflocale_translate
perflocale/detect-languageperflocale_translate
perflocale/convert-urlAny logged-in user — override with the perflocale/abilities/convert_url_permission filter
perflocale/translate-postperflocale_use_mt (per-target edit_post applied inside the callback, plus the creation checks when no translation exists yet)
perflocale/create-translationperflocale_translate (per-target edit_post applied inside the callback, plus the creation checks)

Background Jobs Permissions

The background-processing system runs each operation under the originating user's capability set. Two distinct cap checks happen:

At dispatch time

Each job type declares the capability it needs in its get_required_capability() method. Dispatcher::dispatch() checks current_user_can() against that cap; if the caller lacks it, the dispatch returns ['mode' => 'denied'] without queueing anything.

Job typeRequired capability
string_scanperflocale_translate
data_import, data_exportperflocale_import_export
bulk_translate, site_translateperflocale_manage_translations (worker also re-checks edit_post per source, and edit and read rights on the original before creating a translation)
bulk_string_translateperflocale_use_mt
wpml_migration, polylang_migration, translatepress_migrationmanage_options (WP core)

At worker execution time

When the async worker fires (possibly minutes after dispatch), it re-checks the cap against the originating user ID stored on the job row — not against the user running the cron / Action Scheduler context (which is typically user 0). If the originating user has been deleted or had their role downgraded between dispatch and execution, the job is force-failed with "Permission revoked or dispatching user no longer has access."

This is defense-in-depth against a user's translator cap being removed while a job they dispatched is still in flight.

When managing other users' jobs

Mutation endpoints (cancel, retry, delete via REST or CLI) check the dispatching user's identity against the current user:

  • If you dispatched the job yourself: you can cancel / retry / delete it.
  • If someone else dispatched it: you need perflocale_manage_translations (the supervisor cap).

The list / get endpoints also redact the args field for jobs you didn't dispatch unless you hold the supervisor cap. Use case: in a team where translators dispatch their own scans, a translator can see other translators' jobs are in flight but can't read their args (which may contain file paths) or interfere with them.

Programmatic Usage

Check capabilities in PHP

use PerfLocale\Admin\TranslatorRole;

// PerfLocale caps are registered on WordPress roles, so check them with the
// standard core current_user_can() function.
if ( current_user_can( 'perflocale_translate' ) ) {
	// User can translate.
}

if ( current_user_can( 'perflocale_manage_languages' ) ) {
	// User can manage languages.
}

// The canonical list of custom capabilities is exposed as a public constant.
$all_caps = TranslatorRole::CAPABILITIES;

Grant a capability to a custom role

$role = get_role( 'my_custom_role' );

if ( $role ) {
	$role->add_cap( 'perflocale_translate' );
	$role->add_cap( 'perflocale_use_mt' );
}

Remove all PerfLocale capabilities

On plugin deactivation, call:

PerfLocale\Admin\TranslatorRole::remove_roles();

This removes the Translator role and all custom capabilities from all roles.

Hooks

perflocale/roles/editor_caps

Filters the capabilities granted to the Editor role when the plugin activates. Return an empty array to prevent Editors from receiving any PerfLocale capabilities. Return a subset to restrict them to specific ones. The Administrator role is unaffected by this filter.

// Remove ALL PerfLocale capabilities from the Editor role.
add_filter( 'perflocale/roles/editor_caps', '__return_empty_array' );

// Grant only translation access to Editors (no bulk-translate, no approve rights).
add_filter( 'perflocale/roles/editor_caps', function ( array $caps ): array {
	return [
		'perflocale_translate' => true,
		'perflocale_use_mt'    => true,
	];
} );

Note: Capability grants are written to the database the first time the plugin activates (version-gated). If you add this filter to an existing install, reset the grant by running delete_option('perflocale_caps_version') in a one-time hook, then let WordPress reload - the filter will apply on the next admin_init.

perflocale/roles/cap_roles

Filters which WordPress roles have PerfLocale capabilities removed on plugin deactivation or uninstall. This filter fires in three places - TranslatorRole::remove_roles() (deactivation), and both the full-wipe and preserve-data branches of uninstall.php.

// Do NOT strip caps from the Editor role on deactivation / uninstall.
// Useful if you manage editor access separately and don't want it wiped.
add_filter( 'perflocale/roles/cap_roles', function ( array $roles ): array {
	return array_diff( $roles, [ 'editor' ] ); // keeps ['administrator']
} );

// Extend cleanup to a custom role that was granted caps programmatically.
add_filter( 'perflocale/roles/cap_roles', function ( array $roles ): array {
	$roles[] = 'shop_manager';
	return $roles;
} );