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
| Level | Capability | Used By |
|---|---|---|
| Public | None | GET /languages, GET /languages/{slug} |
| Translate | perflocale_translate | Translation CRUD, strings list, workflow |
| Machine Translation | perflocale_use_mt | POST /machine-translate |
| Languages | perflocale_manage_languages | Language create/update/delete, MT test |
| Import/Export | perflocale_import_export | Import, XLIFF export/import |
| Admin | manage_options | String 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/languagesReturns 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/languagesRequires: 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/reorderRequires: 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}/languageTag 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/usersReturns 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/stringsRequires: 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/scanRequires: 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-translateRequires: 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-translateRequires: 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:
50000characters. - 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 viaperflocale/mt/rate_limit). - When
source_langequalstarget_lang, returns the input unchanged.
Translate a Block from its Source Sibling
POST /perflocale/v1/block-translate/from-sourceRequires: 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 triescontent / text / value / caption / summary / alt / title / placeholderin 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/uploadRequires: 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/batchRequires: 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/cleanupRequires: perflocale_import_export
Removes temporary import files.
XLIFF
Export XLIFF
POST /perflocale/v1/xliff/exportRequires: 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/importRequires: 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/jobsRequires: 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}/cancelRequires: 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}/retryRequires: 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/webhooksRequires: 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/webhooksRequires: 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" }