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.
- 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 theperflocale/mt/agency_async_responsefilter to supply a deferred translation (for example by matching therequest_idagainst 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 theWP_AI_SUPPORTconstant or thewp_supports_aifilter is honoured). The wrapper appliesusingTemperature/usingMaxTokens/usingProvider/usingSystemInstructionvia the WP 7.0 fluent-builder pattern and finalises with->generateText(). When no underlying AI provider is configured the builder returns aWP_Error; PerfLocale converts that to aRuntimeExceptionso the circuit breaker classifies it correctly. Custom routing is available viaperflocale/mt/wp_ai_client_resolver— see the hook docs.
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.
- Environment variable — e.g.
PERFLOCALE_DEEPL_API_KEY. Recommended for containers, CI, and managed hosts. Highest priority. wp-config.phpconstant — same canonical name (define( 'PERFLOCALE_DEEPL_API_KEY', '…' )). Recommended when env vars aren’t available.- Settings → Addons → Machine Translation — paste into the corresponding field. Stored in the
perflocale_settingsoption. 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). Failures don’t block the source publish; they’re logged and retryable 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 at the moment a translation is created by hand.
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.
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).
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.
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); 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, since their volume scales with how many fields a site has. The switch is the mt_meta_custom_fields setting (off by default); like the cap flag above, it has no admin field and is set on the perflocale_settings option.
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). Developers can add their own keys with the perflocale/mt/translatable_meta_keys filter.
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. Three tags core permits are dropped (object, textarea, button — no legitimate reason for a translation to introduce them) 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. 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' ).