---
title: "Machine Translation - Documentation - PerfLocale"
description: "Pick an MT provider - DeepL, Google, Microsoft, LibreTranslate, an agency, or the WordPress AI Client - add keys, and translate on publish or in bulk."
canonical: "https://perflocale.com/docs/machine-translation/"
source: "https://perflocale.com/docs/machine-translation/"
format: "markdown"
---
# 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](https://www.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 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 `usingTemperature` / `usingMaxTokens` / `usingProvider` / `usingSystemInstruction` via the WP 7.0 fluent-builder pattern and finalises with `->generateText()`. 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](https://perflocale.com/docs/hooks/#perflocale-mt-wp-ai-client-resolver).

## 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](https://perflocale.com/docs/api-key-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](https://perflocale.com/docs/background-jobs/) 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.

```php
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](https://perflocale.com/docs/hooks/#reliability-breakers). Disable the whole subsystem (for debugging) via `add_filter( 'perflocale/breaker/disabled', '__return_true' )`.

## Related

-   [API Key Constants](https://perflocale.com/docs/api-key-constants/)
-   [Background Jobs](https://perflocale.com/docs/background-jobs/)
-   [WP-CLI Commands](https://perflocale.com/docs/wp-cli/)

[← Back to Docs](https://perflocale.com/docs/)
