REST API

PerfLocale exposes a RESTful API under the perflocale/v1 namespace for managing languages, translations, strings, imports, and more.

All endpoints require authentication via WordPress cookies or application passwords. Write operations require appropriate capabilities.

Base URL: /wp-json/perflocale/v1/

Authentication & Permissions

LevelCapabilityUsed By
PublicNoneGET /languages, GET /languages/{slug}
Translateperflocale_translateTranslation CRUD, strings list, workflow
Machine Translationperflocale_use_mtPOST /machine-translate
Languagesperflocale_manage_languagesLanguage create/update/delete, MT test
Import/Exportperflocale_import_exportImport, XLIFF export/import
Adminmanage_optionsString scan, webhooks

All write endpoints are protected by WordPress REST nonce verification.

Restricting Public Reads

The language endpoints (GET /languages, GET /languages/{slug}) are public by default because every field they expose - slug, locale, name, native name, flag, RTL flag, default flag - is already rendered to anonymous visitors via the language switcher, hreflang tags, and URL structure. There is nothing private to protect.

Site owners who still want to require authentication for these reads can hook the perflocale/api/languages_public filter. When it returns false, both endpoints require the read capability (any logged-in user). Write endpoints are unaffected - they always require perflocale_manage_languages.

// Drop this in a MU-plugin or functions.php to lock down /languages.
add_filter( 'perflocale/api/languages_public', '__return_false' );

The GET /config endpoint is already opt-in: the route is only registered when Edge Worker Integration is enabled in Settings → Advanced. On a fresh install it responds with 404.

Languages

List Languages

GET /perflocale/v1/languages

Returns all configured languages. Public by default - no authentication required. Can be gated behind login via the perflocale/api/languages_public filter.

Response:

{
	"success": true,
	"data": {
		"languages": [
			{
				"id": 1,
				"slug": "en",
				"locale": "en_US",
				"name": "English",
				"native_name": "English",
				"flag": "en",
				"is_default": false,
				"is_active": true,
				"text_direction": "ltr"
			}
		]
	}
}

Get Single Language

GET /perflocale/v1/languages/{slug}

Parameters:

  • slug (string, required) - Language slug (e.g. en, fr, de).

Create Language

POST /perflocale/v1/languages

Requires: perflocale_manage_languages

Body (JSON):

{
	"slug": "fr",
	"locale": "fr_FR",
	"name": "French",
	"native_name": "Français",
	"flag": "fr",
	"is_active": true,
	"text_direction": "ltr"
}

Update Language

PUT /perflocale/v1/languages/{slug}

Requires: perflocale_manage_languages

Body: Same fields as create (partial updates supported).

Delete Language

DELETE /perflocale/v1/languages/{slug}

Requires: perflocale_manage_languages

Reorder Languages

POST /perflocale/v1/languages/reorder

Requires: perflocale_manage_languages

Body:

{
	"order":  [3, 1, 2, 4],
	"offset": 0
}

order is a list of language IDs in the desired display order. offset is optional (default 0) and lets you reorder a contiguous slice without sending the full list — useful for paginated UIs.

Response:

{
	"success": true,
	"data": { "reordered": 4 }
}

Translations

Get Translations for a Post

GET /perflocale/v1/translations/post/{id}

Requires: edit_post capability for the specific post.

Response:

{
	"success": true,
	"data": {
		"languages": [
			{
				"slug": "en",
				"language_id": 1,
				"name": "English",
				"native_name": "English",
				"is_current": true,
				"is_default": false,
				"has_translation": true,
				"post_id": 42,
				"status": "published",
				"status_label": "Published",
				"edit_url": "https://example.com/wp-admin/post.php?post=42&action=edit",
				"workflow": null,
				"assigned_to": 0,
				"priority": "normal",
				"deadline": ""
			},
			{
				"slug": "de",
				"language_id": 3,
				"has_translation": false,
				"post_id": null,
				"status": null
			}
		]
	}
}

Also supports terms:

GET /perflocale/v1/translations/term/{id}

Create Translation

POST /perflocale/v1/translations/post/{id}

Requires: edit_post capability for the source post.

Body:

{
	"target_lang": "de",
	"copy_content": true
}

Response:

{
	"success": true,
	"data": {
		"post_id": 99,
		"edit_url": "https://example.com/wp-admin/post.php?post=99&action=edit"
	}
}

Update Translation

PUT /perflocale/v1/translations/post/{id}/{lang}

Requires: edit_post capability for the translated post.

Body (partial updates supported):

{
	"title": "Translated Title",
	"content": "<p>Translated content.</p>",
	"excerpt": "Short description",
	"status": "publish"
}

Delete Translation

DELETE /perflocale/v1/translations/post/{id}/{lang}

Requires: delete_post capability for the translated post.

Permanently deletes the translated post and removes the translation link.

Assign Language to Existing Post

POST /perflocale/v1/translations/post/{id}/language

Tag a previously-untagged post with a language so it joins (or seeds) a translation group. Only valid for posts that have no language yet — to prevent accidental re-parenting of an existing translation group, posts that already belong to a group return already_assigned (HTTP 409).

Requires: edit_post capability for the post. Only {type}=post is accepted; terms cannot be reassigned this way.

Body:

{ "slug": "de" }

Response:

{
	"success": true,
	"data": { "post_id": 42, "slug": "de" }
}

Workflow

Only available when the workflow feature is enabled in settings.

Update Workflow Status

PUT /perflocale/v1/workflow/{post_id}/{language_id}

Requires: perflocale_manage_translations

Body:

{
	"status": "assigned",
	"assigned_to": 5,
	"priority": "high",
	"deadline": "2026-04-15",
	"notes": "Please translate by end of week."
}

Available statuses: unassigned, assigned, in_progress, review, approved, published

Get Translators

GET /perflocale/v1/workflow/users

Returns users with perflocale_translate or perflocale_manage_translations capabilities.

Response:

{
	"success": true,
	"data": {
		"users": [
			{ "id": 5, "name": "Jane Translator" }
		]
	}
}

Strings

List Translatable Strings

GET /perflocale/v1/strings

Requires: perflocale_translate

Query Parameters:

  • domain (string) - Filter by text domain (e.g. woocommerce).
  • per_page (int) - Results per page. Default: 50, max: 100.
  • offset (int) - Pagination offset. Default: 0.

Response:

{
	"success": true,
	"data": {
		"strings": [
			{
				"id": 1,
				"domain": "woocommerce",
				"context": "",
				"original": "Add to cart",
				"file_path": "templates/single-product/add-to-cart.php",
				"line_number": 42
			}
		]
	}
}

Scan for Strings

POST /perflocale/v1/strings/scan

Requires: manage_options

Body:

{
	"target": "plugins",
	"domain": "woocommerce"
}

Available targets: theme, plugins, parent

Machine Translation

Only available when machine translation is enabled in settings.

Translate a Post

POST /perflocale/v1/machine-translate

Requires: perflocale_use_mt

Body:

{
	"post_id": 42,
	"target_lang": "de",
	"provider": "deepl"
}

Available providers: deepl, google, microsoft, libretranslate (depends on configuration).

Translate a Single Text Block

POST /perflocale/v1/block-translate

Requires: perflocale_use_mt

Stateless single-string translation - no post, no group, no DB writes. Powers the Gutenberg block-toolbar “Translate with MT” action but usable by any caller that wants to translate a snippet without touching the post store.

Body:

{
	"text": "Hello, world.",
	"source_lang": "en",
	"target_lang": "de",
	"provider": "deepl"
}

Response:

{
	"success": true,
	"data": {
		"translated": "Hallo, Welt.",
		"provider": "deepl"
	}
}
  • Maximum input length: 50000 characters.
  • Provider response is passed through the same wp_kses_post-based allowlist used by post translation, so HTML snippets are safe to apply to block attributes.
  • Shares the per-user hourly budget with /machine-translate (500/hour by default, filterable via perflocale/mt/rate_limit).
  • When source_lang equals target_lang, returns the input unchanged.

Translate a Block from its Source Sibling

POST /perflocale/v1/block-translate/from-source

Requires: perflocale_use_mt + edit_post on target_post_id

Sibling-aware "Fill in from source" endpoint. Locates the corresponding block in the post's source-language sibling using a position-path walk, extracts the relevant text attribute, and machine-translates it into the editing language. Source language is derived server-side from the source post; the caller never has to supply it.

Body:

{
	"target_post_id": 117,
	"block_path":     [3, 1, 2],
	"target_lang":    "de",
	"source_attr":    "",
	"provider":       ""
}
  • target_post_id - the sibling currently being edited (the translation, not the source).
  • block_path - position path into the block tree: [3, 1, 2] = top-level index 3, then inner index 1, then inner index 2. The server walks the same indices in the source post's tree.
  • target_lang - language slug of the post being edited.
  • source_attr - optional. Which attribute to extract from the source block (e.g. "content"). When omitted, the server tries content / text / value / caption / summary / alt / title / placeholder in order and picks the longest-text candidate.
  • provider - optional MT provider override.

Returns the same shape as /block-translate plus the resolved source_attr so the editor knows which attribute to write back.

Import / Export

Upload Import File

POST /perflocale/v1/import/upload

Requires: perflocale_import_export

Body: multipart/form-data with file field import_file.

Returns a token for batch processing.

Process Import Batch

POST /perflocale/v1/import/batch

Requires: perflocale_import_export

Body:

{
	"token": "abc123def456",
	"table": "languages",
	"offset": 0,
	"replace": false
}

Processes 100 rows per batch. Call repeatedly with increasing offset until complete.

Cleanup Import

POST /perflocale/v1/import/cleanup

Requires: perflocale_import_export

Removes temporary import files.

XLIFF

Export XLIFF

POST /perflocale/v1/xliff/export

Requires: perflocale_import_export

Body:

{
	"post_ids": [42, 43, 44],
	"source_lang": "en",
	"target_lang": "de"
}

Returns XLIFF 2.0 XML content.

Import XLIFF

POST /perflocale/v1/xliff/import

Requires: perflocale_import_export

Body:

{
	"xliff": "<?xml version=\"1.0\"?>...",
	"target_lang": "de"
}

Background Jobs

The Jobs API exposes the active background-jobs queue. See the Background Jobs doc for the full feature reference (settings, hooks, GC, recovery). All Jobs endpoints are namespaced under /wp-json/perflocale/v1/jobs/.

Authorisation model: read endpoints require perflocale_translate; mutation endpoints (cancel/retry/delete) additionally require either being the user who originally dispatched the job, or holding the perflocale_manage_translations supervisor cap.

List Jobs

GET /perflocale/v1/jobs

Requires: perflocale_translate

Returns the bounded active-jobs index — never the full per-job payload (which would be expensive to serialize on every admin-page poll). Each row is a compact summary; fetch /jobs/{id} for full detail when you need it.

Response:

{
	"jobs": [
		{
			"id": "5fc94964-29f6-4697-8c4e-64a8ffb5c72d",
			"type": "string_scan",
			"status": "running",
			"progress": 47,
			"updated_at": 1735689640
		}
	],
	"engine": "action_scheduler"
}

Get Single Job

GET /perflocale/v1/jobs/{id}

Requires: perflocale_translate

Returns the full state row including the log ring buffer (last 20 entries), error message (if any), result payload (truncated to 64 KB), and dispatch args.

Response:

{
	"id": "5fc94964-29f6-4697-8c4e-64a8ffb5c72d",
	"type": "string_scan",
	"engine": "action_scheduler",
	"status": "running",
	"created_at": 1735689600,
	"started_at": 1735689601,
	"completed_at": 0,
	"progress": 47,
	"total": 100,
	"processed": 47,
	"attempts": 1,
	"error": "",
	"result": {},
	"log": [ { "t": 1735689601, "m": "Started" } ],
	"created_by": 42,
	"args": { "mode": "directory", "directory": "wp-content/themes/twentytwentyfour" }
}

Args redaction: the args field is included only if you dispatched the job yourself OR you hold perflocale_manage_translations. For other users' jobs, args is replaced with {"_redacted":true} so file paths / sensitive args don't leak across the team. Other fields are returned in full to everyone with perflocale_translate.

Cancel Job

POST /perflocale/v1/jobs/{id}/cancel

Requires: perflocale_translate AND (creator OR perflocale_manage_translations)

Cancels a queued or running job. Returns the updated job state. Long-running workers cooperatively abort on the next progress tick.

Returns 404 rest_not_found if the job ID is unknown, 403 rest_forbidden if the caller can't mutate this specific job.

Retry Job

POST /perflocale/v1/jobs/{id}/retry

Requires: perflocale_translate AND (creator OR perflocale_manage_translations)

Re-enqueues a failed or canceled job. Resets status to queued and schedules a fresh worker event with the original hook + args. Returns 409 rest_invalid_state for non-terminal jobs.

Delete Job

DELETE /perflocale/v1/jobs/{id}

Requires: perflocale_translate AND (creator OR perflocale_manage_translations)

Hard-deletes a job. Removes the row from the active-jobs index AND deletes the per-job option. Only allowed on jobs that have already finished (complete, failed, canceled); running/queued jobs must be canceled first so the runner has a chance to unschedule cleanly.

Webhooks

Register Webhook

POST /perflocale/v1/webhooks

Requires: manage_options

Body:

{
	"url": "https://example.com/webhook",
	"events": ["translation.created", "translation.updated"],
	"secret": "my-secret-key"
}

Available events: translation.created, translation.updated, content.changed

Delivery headers:

  • X-PerfLocale-Signature - sha256=<hmac> for payload verification.
  • X-PerfLocale-Event - event name (e.g. translation.created).
  • X-PerfLocale-Delivery-ID - unique UUID per delivery attempt.
  • X-PerfLocale-Attempt - delivery attempt number (1, 2, or 3).

Retry behaviour: If your endpoint returns a non-2xx status or is unreachable, PerfLocale retries automatically via WP-Cron - after 30 seconds (attempt 2) and after 5 minutes (attempt 3). After three failures the event is written to the perflocale_webhook_failures WordPress option so admins can inspect it. A successful delivery clears any prior failures for that webhook.

List Webhooks

GET /perflocale/v1/webhooks

Requires: manage_options

Delete Webhook

DELETE /perflocale/v1/webhooks/{id}

Requires: manage_options

Error Responses

All error responses follow a consistent format:

{
	"code": "error_code",
	"message": "Human-readable error description.",
	"data": {
		"status": 400
	}
}

Common error codes:

  • rest_forbidden (403) - Insufficient permissions.
  • invalid_type (400) - Invalid object type parameter.
  • not_found (404) - Translation or resource not found.
  • missing_lang (400) - Required language parameter missing.
  • create_failed (500) - Server error during creation.
  • rate_limited (429) - Machine-translation per-user hourly cap reached.

Edge Configuration

Public JSON describing URL mode, default language, detection order, and the full active-language matrix. Used by Cloudflare Workers / Vercel Edge / Netlify Edge to pre-route visitors before PHP.

Availability: only registered when the Edge Worker Integration toggle is on (Settings → Advanced) or the perflocale/edge/enabled filter returns true.

GET /perflocale/v1/config

Public endpoint, no authentication. Response is sent with Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400 and an ETag so edges can revalidate cheaply.

{
	"version": "1.0.0",
	"url_mode": "subdirectory",
	"url_prefix_type": "slug",
	"default_slug": "en",
	"hide_default_prefix": true,
	"excluded_paths": [ "/wp-json/", "/wp-admin/", "/wp-login.php" ],
	"detection_order": [ "url", "cookie", "browser", "default" ],
	"edge_hint_header": "X-PerfLocale-Lang",
	"edge_hint_cookie": "perflocale_edge_lang",
	"languages": [
		{
			"slug": "en",
			"locale": "en_US",
			"hreflang": "en-us",
			"prefix": "en",
			"domain": "",
			"text_direction": "ltr",
			"is_default": true
		}
	]
}

A reference Cloudflare Worker implementation ships in assets/js/edge-helper.js inside the plugin folder.

Translation Memory

Programmatic access to the fuzzy translation-memory store. All endpoints require the perflocale_translate capability except DELETE which is admin-only.

POST /perflocale/v1/translation-memory/suggest

Returns the best exact + fuzzy matches for a snippet. Fuzzy ranking uses similar_text() on up to 100 LIKE-filtered candidates, returning the top N by similarity then usage count.

POST /wp-json/perflocale/v1/translation-memory/suggest
{
	"text": "Thanks for subscribing!",
	"source_lang": "en",
	"target_lang": "fr",
	"limit": 5
}

// Response
{
	"exact": "Благодарим за абонамента!",
	"suggestions": [
		{
			"source": "Thanks for subscribing",
			"target": "Благодарим за абонамента",
			"similarity": 94.5,
			"usage_count": 12
		}
	]
}

GET /perflocale/v1/translation-memory

Paginated browse/search of stored entries.

Query params: source_lang, target_lang, search, page (default 1), per_page (default 50, max 200).

DELETE /perflocale/v1/translation-memory/{id}

Remove one TM entry. Requires manage_options capability.

Glossary

Auto-glossary scanner + candidate review. All endpoints require perflocale_manage_glossary. Only registered when Translation Glossary is enabled (Settings → Addons → Machine Translation).

POST /perflocale/v1/glossary/scan

Run a synchronous scan over published content. Extracts capitalized multi-word phrases that appear ≥ 3 times, filters stop-words and already-in-glossary terms, caches the result in a transient (1 day).

POST /wp-json/perflocale/v1/glossary/scan
{ "limit": 500 }

// Response
{
	"count": 42,
	"candidates": [
		{ "term": "Acme Studio", "occurrences": 18, "example_post_id": 1234 }
	]
}

Post count is clamped to [10, 5000] and further filterable via perflocale/glossary/scan_limit.

GET /perflocale/v1/glossary/candidates

Return the cached candidates from the last scan without re-scanning.

POST /perflocale/v1/glossary/candidates/accept

POST /wp-json/perflocale/v1/glossary/candidates/accept
{
	"term": "Acme Studio",
	"target": "Acme Studio",
	"source_lang": "en",
	"target_lang": "fr",
	"case_sensitive": true
}

Accepts a candidate into the glossary (via Glossary::add()) and prunes it from the candidate cache.

POST /perflocale/v1/glossary/candidates/reject

Remove a candidate from the cache without adding to the glossary.

POST /wp-json/perflocale/v1/glossary/candidates/reject
{ "term": "Not A Brand" }