Machine Translation

Machine translation (MT) lets you auto-translate new posts, existing posts in bulk, or untranslated strings with one click. It is off by default: enable it on the PerfLocale → Addons screen, and a Machine Translation subtab then appears under PerfLocale → Settings → Addons. PerfLocale supports six providers; pick the one that fits your quality, budget and compliance needs.

Providers

  • DeepL - best quality for European languages + Japanese. Paid (with a free tier: 500k chars/month). EU data-residency by default. Start: deepl.com/pro. The DeepL Formality setting is sent as prefer_more / prefer_less, so target languages that don't support formality ignore it instead of erroring.
  • Google Cloud Translation - broadest language coverage (130+). Paid, metered. Google Cloud account required. Higher throughput than DeepL.
  • Microsoft Azure Translator - comparable quality to Google; cheaper for high volume; requires Azure account. Custom Translator models supported.
  • LibreTranslate - open-source, self-hosted. No per-character cost; you run the server. Quality is decent for EU languages, variable for non-EU. Best for privacy-sensitive or offline sites. A provider URL on this machine (localhost) or on a private, shared or reserved address is refused, so for a server on this machine (for example in Docker) or on your own network - a LAN address, or a Tailscale or carrier-grade NAT address in 100.64.0.0/10 - add its host to the perflocale/mt/trusted_hosts filter. A server that translates on the CPU can need more than the 30 seconds PerfLocale waits for each answer on a long post: raise the timeout with the perflocale/mt/request_args filter. In a background job’s web request a call still ends by the slice’s time limit (perflocale/jobs/web_slice_max_seconds, at most 90 seconds); wp perflocale translate waits the full timeout.
  • External Translation Agency - generic HTTP endpoint for sending content to a human-translation service or custom workflow. You configure the URL + auth; PerfLocale POSTs the payload and expects the translated text in the immediate JSON response ({"translation": "…"}). Asynchronous callback workflows are not implemented in core; addons can hook the perflocale/mt/agency_async_response filter to supply a deferred translation (for example by matching the request_id against a callback received out-of-band).
  • WordPress AI Client (WP 7.0+) - delegates translation to whatever AI provider the host site already has configured under WordPress 7.0’s core AI Client. No separate MT API key - PerfLocale reuses the OpenAI / Anthropic / Ollama / etc. provider you already pay for. Feature-detected via function_exists('wp_ai_client_prompt') + wp_supports_ai() (so a host disabling AI per-request via the WP_AI_SUPPORT constant or the wp_supports_ai filter is honoured). The wrapper applies usingModelPreference / usingTemperature / usingMaxTokens / usingProvider / usingSystemInstruction / usingRequestOptions via the WP 7.0 fluent-builder pattern and finalises with ->generateText(). See below for the model it asks for. When no underlying AI provider is configured the builder returns a WP_Error; PerfLocale converts that to a RuntimeException so the circuit breaker classifies it correctly. Custom routing is available via perflocale/mt/wp_ai_client_resolver - see the hook docs.

Keys and addresses. A LibreTranslate or agency key is sent only to the address it was entered for: changing that provider’s address clears its stored key, so enter the key again for the new address. On multisite, when a provider’s key is set by the network (in wp-config.php, the environment or Connectors), only a super admin can change that provider’s address. A settings import run by a site administrator keeps the stored address and says so in the import report: “Not imported: LibreTranslate URL. This provider’s key is set by the network administrator; ask them to change the address.”

WordPress AI Client: model, readiness and retries

  • Model. PerfLocale asks for a small, low-cost model: an ordered list of OpenAI mini, Anthropic Haiku and Google Flash models. WordPress uses the first one a connected provider offers, and picks a model itself when none of them is available. To choose another model, set model in the perflocale/mt/wp_ai_client_args filter: one model ID, or an ordered list; null or an empty array leaves the choice to WordPress.
  • Temperature. Requests are sent with a temperature of 0.2. When a model refuses the temperature setting, the request is sent once more without it, and later requests to that model in the same page load or job leave it out. That first refusal does not count toward the circuit breaker.
  • Readiness. The block editor and Tools → Site Health ask WordPress whether a connected provider can generate text. The answer is kept for 10 minutes when it is yes and 1 minute when it is no, and is asked again straight away when a provider or its key changes (on the Connectors screen, or through an environment variable or constant). With no provider connected, a translation fails with “No AI provider is connected. Connect one under Settings → Connectors.”
  • Connectors link. When the WordPress AI Client is the selected provider, the Machine Translation settings show a link to Settings → Connectors, where WordPress keeps the AI provider and its key. The link appears only on WordPress versions that have that screen. When no connected provider is ready, the Tools → Site Health check and the editor’s Set up machine translation link point there too.
  • Timeouts. A request from the block editor waits up to 10 seconds, any other up to 60, on WordPress versions whose AI Client accepts request options.

The separate “AI” plugin

The plugin named AI is separate from WordPress core and from PerfLocale. If you use it, two of its switches matter:

  • Content Translation rewrites the post open in the editor in place, replacing its text with a translation. It ignores blocks marked “Do not translate” and knows nothing about PerfLocale’s languages or translations. Leave it off on a PerfLocale site.
  • Connector Approval, when on, blocks AI requests from any plugin that has not been approved on its approval page. Approve PerfLocale there, or translations through the WordPress AI Client provider fail. PerfLocale treats that refusal like a rejected API key: the circuit breaker opens at the first one.

API keys

Every paid provider requires an API key. PerfLocale resolves it from three sources in priority order - env var > wp-config.php constant > database setting. The first non-empty source wins.

  1. Environment variable - e.g. PERFLOCALE_DEEPL_API_KEY. Recommended for containers, CI, and managed hosts. Highest priority.
  2. wp-config.php constant - same canonical name (define( 'PERFLOCALE_DEEPL_API_KEY', '…' )). Recommended when env vars aren’t available.
  3. Settings → Addons → Machine Translation - paste into the corresponding field. Stored in the perflocale_settings option. Used when neither an env var nor a constant is set.

See API Keys: Environment Variables & Constants for the full canonical-name table and examples.

Auto-translate on publish

Toggle Auto-Translate on Publish to trigger MT when a source post is published. PerfLocale creates a translation shell for each target language and populates it via the MT provider in the background (Action Scheduler when it’s available, WP-Cron otherwise). Each published post gets its own auto_translate job on the Jobs page, and the jobs run one at a time: a job translates one language after another in short slices and continues after a stopped worker, like a bulk translation. When the provider does not answer in time in 3 slices in a row for one language (perflocale/jobs/max_unit_stops), that language is not translated and the job goes on with the next one; the reason is “The translation provider did not answer in time in 3 runs in a row, so this language was not translated.” Failures don’t block the source publish. When a language could not be machine-translated, the post’s edit screen says so once, on the next visit of the user who published the post (or of its author, when a schedule or WP-CLI published it), for up to seven days: for example “Machine translation was not saved: <reason> The Finnish translation was not changed.” A password-protected post is not sent, and the notice says “The post is password-protected, and machine translation does not send password-protected posts when they are published.” A later run that translates every language removes the notice. You can retry from the Translations page.

Auto-Translate Target Languages narrows which languages that applies to: unchecked languages still get a clean empty stub for a translator to work from. Leaving every language checked keeps newly-added languages included automatically. A companion Auto-Translate on Create toggle does the same for title, content and excerpt when a translation is created with the + Create link; see Auto-translate on Create.

Auto-translate on Create

With Auto-Translate on Create on, clicking the + Create link in a Translations box (the classic editor’s, or the one PerfLocale adds to the Contact Form 7 and WPForms form editors) creates the translation, fills its title, content and excerpt through the provider when the conditions below are met, and opens it in the editor. The link only ever makes a new translation. If that language already has one (for example, someone created it after the page was loaded), it is opened as it is: it is not created a second time and not machine-translated again, so nothing a translator has written there is overwritten.

The + Create button in the block editor’s Translations panel does not use this setting. It always creates a copy of the original and offers a link to open it. To machine-translate that copy, open it and use Translate from <language> in its Block translation panel. That button sends one request per block, and each request counts against the hourly machine-translation request limit described below, so a long post uses more of it than a single + Create does.

Machine translation on Create runs only when the person clicking has all three of these:

  • the perflocale_use_mt capability, which grants machine translation;
  • WordPress’s own permission to edit the post being translated (edit_post), because the post’s whole content is sent to the provider;
  • room under the hourly machine-translation request limits (500 per user and 5,000 site-wide by default, shared with the REST endpoints). A Create that uses machine translation counts as one request.

It also runs only for a language checked under Auto-Translate Target Languages (every language is checked unless you narrow the list). For an unchecked language, + Create makes a clean stub whether or not Auto-Translate on Create is on, and even when machine translation is switched off: the title, slug, post meta, featured image and terms are copied, and the content and excerpt are left empty for a translator working from scratch.

If the language is checked but the capability or the edit permission is missing, + Create still creates the translation as a copy of the original for a translator to work from, and the provider is not contacted. When machine translation was set to run and did not fill the translation, + Create creates the same copy, and the screen it opens on (the block editor, the classic editor, or the Translations screen when the new post has no edit screen) says why:

  • when the result does not keep the block structure of the source (see Block markup is checked before saving): “The machine translation was not saved because … This translation is an untranslated copy of the original.”;
  • for any other reason, such as a provider error or the hourly request limit: “Machine translation was not saved: <reason> This translation is an untranslated copy of the original.”, for example with the reason “The machine translation returned no translation.”;
  • for a password-protected original, which is not sent when a translation is created (see Password-protected posts): “Machine translation was not saved: The original is password-protected, and machine translation does not send password-protected posts when a translation is created. This translation is an untranslated copy of the original.”

The notice is shown once, to the person who clicked + Create.

A new translation is copied from the post in the site’s default language when the translation group has one, which is not always the post you clicked + Create on. If your account cannot both edit and read that original, + Create is refused with “You cannot edit the original this translation is copied from.” before anything is sent to the provider. The /machine-translate REST routes apply the same check when they would create a translation, before the request counts against the rate limit. Permissions covers what else creating a translation requires.

When machine translation of a post sets out to create its translation and finds, at the moment of creating it, that the translation already exists (for example because another request created it while the provider was working), it leaves that translation as it is and does not write its output there. Bulk translate, WP-CLI and the /machine-translate REST routes report this as “This translation already exists, so the machine translation was not written to it.” + Create opens the existing translation instead. (For a term, the /machine-translate/object route answers with the status exists instead, or writes its output over the term when the request asks to overwrite and you may edit that term.)

Password-protected posts

Before it sends a password-protected post’s text to the provider, every machine-translation path asks the perflocale/mt/send_password_protected filter, starting from that path’s default:

  • Not sent by default - the automatic runs and the runs over every post. Auto-Translate on Publish does not translate the post, Auto-Translate on Create makes a copy of the original for a translator to work from, the site-wide run (Translate the entire site and the REST site_wide mode) counts its pairs as skipped, and wp perflocale translate --all, also with --async, counts the post as skipped.
  • Sent by default - the paths where someone picks the posts: the editor (Fill in from source and the block toolbar), the bulk action on posts selected on the Translations screen, the machine-translation REST routes, the perflocale/translate-post ability and wp perflocale translate with a post ID or --post-ids.

A path the filter refuses skips the post: a bulk run or WP-CLI counts it as skipped, wp perflocale translate <id> prints a warning, and the editor, the REST routes and the ability answer “This post is password-protected and was not sent for machine translation.” Return true from the filter to let every path send such posts, or false to keep them out of every path.

Bulk translate

From the Translations page, multi-select rows and pick Translate via <provider> from the Bulk actions dropdown (the label carries the active provider’s name, e.g. “Translate via DeepL”). Small batches run inline; larger ones queue as a bulk_translate background job visible under PerfLocale → Jobs. The sync/async split is governed by the bulk_translate threshold under Settings → Performance → Background Thresholds (default 25, counted as source posts × target languages). The monthly character limit applies either way, as do the hourly request ceilings on the REST endpoints (500 per user and 5,000 site-wide by default, both filterable). For very large batches you can also use WP-CLI: wp perflocale translate --all --to=de --post-type=post --skip-existing.

Starting a bulk post translation, a bulk string translation or a site-wide run counts as one request against the hourly request limits, and a start they refuse does not run. A queued bulk_translate job runs in slices of at most 20 seconds or 500 pairs per background request and continues where it stopped, so a host’s request time limit does not stop it part-way (see Background Jobs).

When a bulk action is refused (for example by the monthly limit) or stops with an error before its first item, the Translations page says “Bulk machine translation did not run.” with the reason. Items you are not allowed to translate are counted separately from items that already have a translation. A post is never translated into its own language: that request is refused with “Source and target languages match; nothing to translate.” The site-wide run and wp perflocale translate --all take default-language posts (and posts with no language yet) as their sources, never translations.

Before anything is sent to the provider, the bulk action shows a character estimate for the selection (items, characters, and how many already-translated pairs will be skipped) and asks for confirmation. A "Translate the entire site" panel on the same page starts a resumable background chain over every published post of the selected post types - it requires an estimate first, runs in bounded chunks, never overwrites existing translations, and can be cancelled from the Jobs page at any time. Dispatches that would exceed the monthly character limit are refused up front (see below). The estimate counts the title, content and excerpt, plus the meta fields that would actually be sent (see SEO meta and custom fields). It leaves out a WooCommerce product’s variation descriptions and custom attribute options, which are translated with the product (see WooCommerce); the monthly limit is checked for them when they are sent.

When a bulk or site-wide run has created translations that kept the source-language content because a “Do not translate” section was lost, the notice on the Translations screen (for a run that finishes at once) and the job’s row on the Jobs page say how many, with the first warning.

A bulk translation, the site-wide run and wp perflocale translate check between items whether other requests changed languages, settings or slugs, and continue with the current values. When the target language is deleted during the run, the new translations the run created for it in its current batch of up to 500 pairs are deleted again and counted as failed, and the remaining pairs fail with “The target language was deleted or deactivated while the run went on; this pair was not translated.” Translations from batches the run already finished, and translations that existed before the run, are kept. When the language is deactivated, the remaining pairs fail the same way and the translations already written stay.

Monthly character limit

Optional cap on MT usage per calendar month. Hit the cap → auto-translate + bulk translate pause until next month (manual translate via the editor button still works, since you’re explicitly opting in). Tools → Site Health flags the usage as a recommendation at 80% of the limit and as critical at 95%.

Bulk operations are checked against the limit before they start: a bulk or site-wide dispatch whose estimate would exceed the remaining monthly budget is refused with the numbers in the message, instead of burning quota until the cap trips mid-run. The check is governed by the mt_enforce_cap_on_bulk setting (on by default). It has no admin field - set it to false in the perflocale_settings option if you prefer the run-until-cap behaviour.

The Taxonomy Translations tool and the WooCommerce Create Page Translations button send names and titles through the same limit. A page title that was sent once is not sent again on a later click, unless the original title changed.

SEO meta and custom fields

Machine translation can also cover meta fields on the translated post. SEO titles, descriptions, and social text from the supported SEO plugins are translated by default (template variables like %title% or %%sitename%% are protected and pass through unchanged, and a value made only of such variables is copied as it is and not sent); focus keywords are deliberately left untouched. That default lives in the mt_meta_seo setting.

Text fields from ACF, Meta Box, and Pods are not translated unless you opt in: tick Custom Fields in the Machine Translation settings (the mt_meta_custom_fields setting, off by default). Field values are often names, SKUs, URLs or numbers that must not be translated, so turn it on only when your custom fields hold prose.

Two safety rules apply everywhere: a value a translator has already edited is never overwritten, and a translation that loses a placeholder is rejected (the field keeps its source value and the reason is recorded on the translation post). For WooCommerce products, variation descriptions and custom attribute options are translated too (see WooCommerce). Developers can add their own keys with the perflocale/mt/translatable_meta_keys filter.

These fields are skipped and left as they are:

  • a field someone changed on the translation while the provider was working - that edit is kept;
  • a field emptied on purpose on the translation, where the integration records that (Meta Box);
  • an ACF repeater or flexible-content row whose structure on the translation differs from the source, for example after rows were reordered there;
  • a field stored as several rows.

Fields whose names look like credentials (_password, _token, _api_key and the rest of the perflocale/translation/dangerous_meta_patterns list) are never sent to a provider, whichever integration registered them. Machine translation also leaves out every field whose name contains token, password, api_key, apikey, secret, license or licence as a whole word anywhere in the name, such as stripe_token or my_license. Those fields are still copied between languages when a translation is created and by Sync Fields.

Blocks marked “Do not translate”

In the block editor, the PerfLocale menu in a block’s toolbar has Mark “Do not translate”. The menu is offered on blocks that hold text, such as paragraphs, headings, lists and quotes, and not on layout blocks such as Group or Columns. When a whole post is machine-translated (on publish, on Create, in bulk, with wp perflocale translate, or through the /machine-translate REST routes), each marked block that has content is swapped for a placeholder before the request goes out: its markup, its attributes and every block inside it stay out of the request. The block is then put back unchanged in the translation. If the provider’s response leaves out one of the placeholders, the content is not translated in that run, so nothing marked is lost: a new translation gets the original content, and an existing translation keeps the content it has. The title and excerpt are still translated. A line goes to the PHP error log, and wp perflocale translate prints the warning “A "Do not translate" section was lost in machine translation, so the content was kept unchanged.”

This holds at any nesting depth. PerfLocale reads the marker on blocks nested up to 50 levels deep. Deeper than that it does not look at blocks one by one: a block there that is marked, or that contains a marked block, is kept out of the request whole. Unmarked blocks inside it therefore stay untranslated too, and a line in the PHP error log says so.

“Do not translate” blocks keep their markup on WordPress 6.7 or newer; on older versions WordPress filters them as usual. A site can change that per save path with perflocale/protected_blocks/keep_markup.

Machine translation leaves out <style> and <script> elements: the translated post gets the text, not the source’s inline styles or scripts. To keep a block’s own styles or scripts in every language on WordPress 6.7 or newer, mark the block “Do not translate”.

The editor’s own block-level actions follow the same rule: a marked block protects itself and every block inside it.

  • On an original, Translate to <language> in the block toolbar and in the Block translation sidebar panel leaves out marked blocks and everything inside them. A marked block offers no translate items in its toolbar. A block inside one shows a disabled item instead, Not translated: inside a “Do not translate” block, which keyboard and screen-reader users can reach.
  • On a translation, Fill in from <language> source is not offered on a block that is marked in the translation; a block inside a marked block shows the same disabled item. When the matching block in the original is marked, or sits inside a marked block, the block is filled with the original’s text unchanged and nothing is sent to the provider.
  • Translate from <language> in the Block translation panel of a translation fills every block the same way, and its message says how many blocks were kept because they are marked.

Each text attribute of a block is translated and written back to its own place, without the block’s HTML around it: an image’s alt text, caption and title, and a quote’s citation (the quote’s paragraphs are blocks of their own).

Block markup is checked before saving

Before a machine translation of a post is saved, PerfLocale compares the translated content with the source: the same blocks in the same order, every block comment complete, and block attributes that can be read. If the result does not match, it is not saved. A bulk translation, wp perflocale translate and the /machine-translate REST routes report the item as failed with a reason such as “The machine translation was not saved because it does not keep the block structure of the source: the result has 11 block delimiters and the source 12.”, and an existing translation keeps the content it has. WooCommerce variation descriptions are checked the same way. The provider has already translated the text by then, so a refused result still counts toward the monthly character limit and the provider’s bill.

Content without block markup is checked for its HTML structure instead: the same structural elements (paragraphs, headings, lists, tables and the like) in the same order, no tag cut off and every element closed. A result that does not keep it is not saved either, with a reason such as “The machine translation was not saved because it does not keep the HTML structure of the source: a <div> element is not closed.” Text without any HTML has no structure to compare.

The translated title and excerpt are checked too. One that looks cut off is not saved, for example “The machine translation was not saved because its title looks cut off: it stops in the middle of a sentence.” The other reasons are “it is much shorter than the original” and “a bracket is not closed”. As with the content checks, nothing of that result is saved.

With Auto-Translate on Create, a refused result still creates the translation as an untranslated copy of the original, and the screen it opens on shows the reason. With Auto-Translate on Publish, a refused result leaves that language’s translation as it is, and the post’s edit screen shows the reason once, for example “The machine translation was not saved because … The Finnish translation was not changed.”

Block comments (<!-- wp:paragraph --> and the settings they carry) are not sent to the provider as they are: each one is replaced by a marker with no text to translate and put back after translation, so the provider never receives a block’s settings. Text that a block keeps only in its comment, such as a label, is sent on its own and written back into the block’s comment; a block comment that holds a shortcode is kept whole, so its text is not translated. In HTML content, shortcode tags are held back the same way (see Privacy). When the provider loses, repeats or changes a marker, the result is not saved and the reason says how many markers came back damaged.

Finding translations with damaged blocks

wp perflocale damaged-blocks reads every translation and lists those whose block markup is damaged: a block comment saved as visible text (&lt;!-- wp:), a cut-off block comment, blocks that are opened and not closed, or a block that lost settings its source block has. It lists a translation only when it has more of such damage than its source, and names the first damaged spot. It changes no content or settings. See WP-CLI for its options and exit status.

To repair a listed translation that nobody edited by hand, translate its source again into that language. Use the source_id and language columns of the list, not post_id: for a row with post_id 812, language de and source_id 640, run

wp perflocale translate --post-ids=640 --to=de

Without --skip-existing, this replaces the content of the German translation (post 812) with a new machine translation, which is checked as above. Several sources of one language go in one list, for example --post-ids=640,655. This replaces any change made to the translation since, so do not use it for a translation someone edited. For a translation someone edited by hand, compare it with its revisions or fix the listed spot in the code editor.

Strings in bulk

On PerfLocale → Strings, the machine-translation toolbar offers Translate selected, Translate filtered and Translate all. A run takes up to 5,000 strings (the perflocale/mt/bulk_string_max_per_dispatch filter changes that), and larger selections run as a bulk_string_translate background job.

  • Continuing a large run. Without Overwrite, Translate filtered and Translate all pick only strings that still miss a translation in a chosen language. When more are left than one run takes, the result says “Still missing a translation: N. Run it again to continue.”, and the next run carries on where this one stopped. With Overwrite, every run starts again from the first 5,000 strings, so narrow the filter to reach the rest.
  • Confirmation. When the filtered or full selection is larger than one run, the confirmation dialog says how many strings this run translates and that running it again continues. Translate selected refuses more selected rows than one run takes. The /machine-translate/estimate REST route counts the same strings as one run.
  • Overwrite and the estimate. With Overwrite, strings that already have a translation are sent again, and the character estimate and the monthly-limit check count them too.
  • Plain text. A string without HTML is translated and stored as plain text, so & and quotes stay as typed.
  • Plural forms. For a language with three or more plural forms (Polish, Russian, Arabic), a plural string with one count placeholder is sent once per form with a sample number in place of the placeholder, and every form is saved. Each extra form is one more text sent to the provider. A form whose translation does not contain the number is not saved and is reported; only a form used for one count alone, such as the Arabic forms for 1 and 2, may leave the number out. Plural strings translated before this are completed only by an Overwrite run.
  • Failures and cancelling. With the WordPress AI Client, LibreTranslate and other providers that take one text per request, a failure fails only that string, and a cancel from the Jobs page takes effect before the next string is sent. DeepL, Google and Microsoft translate a batch of up to 25 strings per request. What was translated before a cancel is kept.
  • Retry. A run that translated nothing and hit a failure ends as Failed on the Jobs page, with the first error, and is not re-run by itself. Retry runs the same selection again.
  • A stopped worker. A background run continues from the string it was on when its worker stopped, and fails after 3 stops at the same string (see Background Jobs).

The toolbar shows the reason when a run is refused, and the first error when strings fail.

Page builder layouts

Machine translation sends a post’s title, content and excerpt. Beaver Builder, Bricks, Elementor, Oxygen and Breakdance keep a page’s layout in their own post data, which is not sent. When that layout is what the page shows, the translation is saved with its title and excerpt translated and the warning “The Elementor layout was not machine-translated. Translate it in Elementor.” The warning is shown wherever the result is: the edit screen, the Translations screen and the Jobs page after a bulk run (with the kept-content count), and wp perflocale translate (“Translated, with the warning above.”). After Auto-Translate on Publish, the post’s edit screen adds “Affected translations: …”. Before a bulk or site-wide run, the estimate says how many items it concerns, for example “The page builder layouts of 3 items are not included: machine translation does not send them.” An add-on for another builder declares its layout with perflocale/mt/builder_documents.

What comes back is sanitised

HTML returned by a provider is run through wp_kses() before it is stored. The allowlist is derived from WordPress’ own wp_kses_allowed_html( 'post' ) rather than hand-maintained, so it tracks core: whatever an editor is allowed to save, a translation is allowed to contain. Two tags core permits are dropped (object and textarea - no legitimate reason for a translation to introduce them), button keeps only the attributes the Button block saves (type with the values button, submit or reset, class, id, style, title, plus dir, lang and aria-label), and source is added so <video> / <audio> markup survives the round trip. wp_kses() also neutralises javascript: URLs and event-handler attributes. Adjust the list with perflocale/mt/allowed_html.

Privacy

When you trigger MT, the plugin sends the post’s title + content + excerpt to the provider you chose, minus any blocks marked “Do not translate” when the whole post is translated. In HTML content (post content, and custom fields and strings that hold HTML), shortcode tags with their attributes are held back and put back unchanged: attributes usually hold IDs, form names, keys or sizes, and a translated attribute breaks the shortcode. The text a shortcode encloses is translated, and so is the caption of a [caption] shortcode; perflocale/mt/text_shortcodes sends other shortcodes whose attributes hold visible text. Titles, excerpts, plain-text fields and strings without HTML are sent as text, shortcodes included. When the provider loses, repeats or changes a held-back shortcode, the result is not saved. See the External Services section of the plugin’s readme for each provider’s ToS / Privacy links + what data is sent. For fully-local translation, use self-hosted LibreTranslate.

Reliability under provider failure

Every MT call goes through a per-provider circuit breaker. When the active provider starts returning 401 / 403 (bad key), 429 (rate limit), or 5xx (transient), the breaker trips after N failures in a sliding window and subsequent calls short-circuit instantly - no more burning 3 retries × up to 30s timeout per row when DeepL or Google is having a bad day.

The default thresholds:

  • Auth errors (rotated / revoked API key) - breaker opens on the FIRST hit. No point retrying.
  • Rate-limit / transient errors - breaker opens after 5 failures in 5 minutes, stays open for 5 minutes, then probes once. Once is literal: the probe holds a short lease, so when the cooldown expires under load exactly one request goes to the provider and the rest keep getting the refusal until that probe reports back.
  • Unusable success responses - a provider or proxy that answers HTTP 200 with something the plugin cannot read (an HTML error page, a captive-portal interstitial, truncated JSON) counts as a failure and trips the breaker like any other. The call was paid for and produced nothing, so treating it as a success would leave a broken endpoint being called forever.

Operator visibility: open breakers appear in Tools → Site Health under "PerfLocale circuit breakers" with a one-click reset link per breaker. The PHP error log also gets a single line on each transition (no spam).

Custom fallback: catch \PerfLocale\Concurrency\BreakerOpenException in your own MT call sites to route to a fallback (a cached string, the source text, a "service unavailable" notice) instead of conflating with genuine downstream errors.

try {
	$translated = $mt_service->translate_text( $text, 'en', 'de' );
} catch ( \PerfLocale\Concurrency\BreakerOpenException $e ) {
	// MT provider currently in cooldown - degrade gracefully.
	$translated = my_cached_lookup( $text ) ?? $text;
}

Tune trip thresholds + cooldown via the breaker hooks. Disable the whole subsystem (for debugging) via add_filter( 'perflocale/breaker/disabled', '__return_true' ).

← Back to Docs