- Security: a user holding the Translator role could publish, privatise or trash any post or page they were able to edit. Translation status changes were gated only on the edit capability, bypassing the publish and delete capabilities that role deliberately withholds. Status changes now map to the target post type's own capabilities, exactly as WordPress core does. Drafting is unchanged.
- Security: translated Contact Form 7 forms rendered the form's stored configuration into the public page — recipient addresses, mail templates and headers included — because Contact Form 7 keeps a flattened copy of every property in the post content. Translated forms now read Contact Form 7's own form property. A machine-translated form reverts to the source markup until it is re-saved in Contact Form 7's editor.
- Security: data export filenames now carry 32 characters of entropy instead of six, exports are published atomically so a partially written file can never be served, and an export's filesystem path is hidden from REST callers without the import/export capability.
- Security: nested credentials and URL user-info are stripped recursively from exports rather than only at the top level, and credential masking in provider errors no longer depends on the credential containing a digit.
- Security: draft, pending, private and trashed translations no longer leak through Yoast breadcrumb structured data or the WooCommerce cart.
- Security: custom machine-translation endpoints are now validated against IPv6 addresses as well as IPv4, and provider responses are size-capped.
- WooCommerce: simultaneous purchases across language versions of the same product no longer lose a stock decrement. Each sale is applied to the sibling languages as a relative change the database performs atomically, so three shoppers buying three language versions of a stock-10 product at the same instant leave every version at 7 — previously two of those three sales could be lost, and the loss grew with the number of simultaneous orders. Stock status and the lookup table WooCommerce uses for shop queries are derived in the same operation that reads the quantity, so a translated product cannot display a figure a concurrent sale has already superseded.
- WooCommerce: exchange-rate syncing merges the provider's response instead of replacing the whole stored set, so a partial response no longer wipes the currencies it omitted. A response that fails validation now counts as a failure for the circuit breaker rather than resetting it. Bulgaria's suggested currency is now EUR.
- Content: protected nested blocks keep their inner blocks, and XLIFF export no longer strips emoji, rare CJK and other supplementary-plane characters.
- Content: a failed table read during export now fails loudly instead of producing a valid-looking but silently incomplete backup.
- Reliability: a failed string-translation move no longer deletes the source rows, and a transient database error while building the link map is no longer cached as an authoritative empty result.
- Reliability: job locks can no longer be released or garbage-collected by a previous owner, and the Action Scheduler integration — including the bridge that marks a job failed when its worker is killed — now actually registers. It previously never did.
- Performance: saving a post no longer rewrites its translations when nothing that is actually synced has changed. Every save used to issue one full update per translation regardless, bumping each translation's modified date, firing the save hooks other plugins listen to, and costing a full set of database writes per translation. Translations are now written only when a synced field really differs; what gets synced is unchanged. The perflocale/cache/flush_object purge signal still fires for each translation, so CDN integrations are unaffected.
- Reliability: a post list containing an entry that is not a post object — WordPress can hand one to its the_posts filters when a post is permanently deleted during a page load — caused a fatal error in the front-end translated-slug preload and in the admin post-list translations column. Both now skip such entries and behave exactly as before for every real post.
- Background jobs: deleting a job now also removes the export file it produced, instead of leaving it in the uploads folder with no record identifying it. An export whose post-write hook throws is no longer orphaned — the export succeeded, so the job keeps its result and reports the hook failure separately.
- Background jobs: when a scheduler refuses an event — a filter veto, a duplicate, or a queue error — that refusal is now reported and logged rather than silently producing no work. On multisite, a job lookup can no longer return another site's job when a single process handles more than one site.
- Performance: data import no longer flushes the entire object cache every 500 rows, which on sites running Redis or Memcached evicted every other plugin's cached data for the duration of the import.
- New filter: perflocale/url/query_var renames the query variable used by query-parameter URL mode, for sites where another plugin already owns 'lang'. Register it from an mu-plugin and flush permalinks afterwards.
- New filter: perflocale/jobs/deduplicate_admission restores the previous behaviour when a second identical background job is dispatched while the first is still in flight.
- New filter: perflocale/breaker/probe_lease_seconds sets how long a single worker holds the half-open probe lease on a tripped circuit breaker.
- Behaviour change: a save that changes none of the synced fields no longer writes to the translations, so save_post, post_updated and transition_post_status no longer fire for them on such a save and their modified dates stay put; exports now fail rather than silently omit a table that cannot be read; per-status translation counts may read higher on sites whose stored status had drifted; and perflocale/mt/pre_translate and perflocale/machine_translation/after now carry the source object's own language rather than the site default.
- Security: the translation endpoint also accepted WordPress's internal post statuses. Setting a translation — or, for the site's default language, a source post — to auto-draft handed it to the WordPress routine that permanently deletes abandoned drafts, so a role deliberately denied the delete capability could still have content destroyed. Internal statuses are now refused; draft, pending and other plugins' workflow statuses are unaffected, and custom post types are judged by their own capabilities rather than the built-in post ones.
- Security: machine translation requested through the Abilities integration bypassed the hourly per-user and site-wide translation limits, and the contention guard, that the REST endpoints enforce. All three entry points now share one admission check.
- Background jobs: deleting a job's export file no longer follows a symbolic link, so it cannot remove a file belonging to a different job, and it verifies the file at the recorded location is still the one the job created. If the file cannot be removed the job is still deleted and the leftover is logged rather than disappearing silently.
- Background jobs: recurring maintenance events now report whether the scheduler accepted them, and a refusal is logged with its reason instead of being indistinguishable from success. Site Health lists all four recurring events it checks, including the machine-translation usage cleanup.
- Background jobs: a long-running command or worker no longer reuses a stale answer about whether one of the plugin's jobs is in flight. On multisite that stale answer could come from a different site and delay Action Scheduler's recovery of other plugins' stuck tasks.
- Security: the WordPress AI Client provider called the SDK's generateText() directly. WordPress applies wp_supports_ai() and the site-wide wp_ai_client_prevent_prompt policy filter only to the snake_case form, so a site that had globally blocked AI prompts was still being prompted by this plugin. The call now goes through the policy layer.
- Routing: query-parameter URL mode now honours the URL Prefix Format setting, so a site set to locale form serves ?lang=en-us rather than ?lang=en everywhere the plugin writes a URL, including the search form's hidden field and the WooCommerce AJAX endpoint. Requests using the other form are permanently redirected to the canonical one, so a language keeps exactly one indexable URL.
- Routing: a language could be made unreachable by its own slug. Locales are also matched in their URL form, and a single-pass lookup let a later language's locale form overwrite an earlier language's real slug — give one language the slug de-de and another the locale de_DE, and /de-de/ served the second one. Real slugs now always win.
- REST API: creating a language with a slug or locale that already exists returned HTTP 500 with a raw database error. It now returns 409 with slug_exists or locale_exists.
- Background jobs: the “is this event already scheduled?” check was a no-op under Action Scheduler, because an async action has no next-run timestamp for it to find, so scheduling guards that relied on it could queue duplicate work. It now asks Action Scheduler directly.
- Security: machine-translation rate limiting now validates the shape of its stored counter, not just its presence. A corrupted counter — from an object-cache collision or another plugin writing the same key — could previously slip past the hourly cap entirely, or turn a translation request into a fatal error.
- Security: a new Site Health check reports whether your server actually protects the export directory. PerfLocale writes a Deny-from-all .htaccess beside every export, but nginx and Caddy ignore .htaccess and PHP cannot tell which server it is behind. The check places a temporary random file there, requests it over HTTP, and raises a critical result with the exact nginx and Caddy snippet if the file comes back.
- Reliability: when the scheduler refuses a webhook retry, that refusal is recorded as a delivery failure instead of being discarded. Previously the retry vanished with no second attempt and no entry in the failure log. Failure-log lock contention is now reported rather than silently skipped.
- WooCommerce: block Cart and block Checkout work again on subdirectory multisite children. A shopper browsing in a non-default language made every Store API request return a 404 there, which broke the cart. Store API messages on those sites now come back in the site's default language; single sites, subdomain and per-domain networks are unaffected.
- Background jobs: deleting a job created before 1.0.1 no longer removes whatever file happens to sit at that job's old export path — those records predate the file-identity check, so the artifact is left to the age sweep instead. The failure log also no longer records the export path, filename or download token, only the job ID and a short hash.
- REST API: a status WordPress refuses for a post type now returns 400 translation_status_rejected instead of 500. Nothing is written either way; the previous response told clients the server had broken when the request was at fault.
- Caching: new perflocale/cache/purge_urls action fires when a post's public visibility changes, carrying every affected front-end URL including each translation's. A translated page is a separate URL that a full-page cache usually does not know about, so one that had been public could stay readable in the cache after being made private.
- Backup and restore: a replace-mode import is now all-or-nothing. If the database refuses even one row the whole import rolls back and your existing data is untouched, instead of committing everything that did land and reporting success. Settings, add-on settings, the disabled-add-on list and role grants carried in the bundle roll back with it. A PO import run with replace behaves the same way.
- Backup and restore: every write to an export file is checked. A disk that fills up, a quota that runs out, or a network share that drops a write partway through no longer produces a file that ends correctly and is corrupt in the middle — the export is abandoned and your previous backup is left in place.
- Background jobs: a worker that cannot record itself as running no longer runs the job anyway. A failed status write used to leave the job listed as queued while its work went ahead — translations created, provider credit spent — and the queued row stayed eligible to be run a second time.
- Background jobs: dispatching the same job twice — a double-clicked button, a retried request, the same --async command run again — now returns the job already in flight instead of queueing a second one. Different arguments, a different job type and the chunked site-translation chain all keep running in parallel.
- Machine translation: the circuit breaker now counts a provider that answers with something unreadable. A proxy returning HTTP 200 with an error page used to clear the failure count on every call, so the breaker could never trip. When the breaker opens and its cooldown expires, exactly one request is now let through to test the provider rather than every request that arrives at once.
- WP-CLI: import, po-import and network-import exit non-zero when the import reports errors. A malformed file previously printed a warning and then Success: with exit code 0, so a restore script could record a restore that never happened.
- REST API: malformed XLIFF — an empty body, XML that does not parse, a document declaring entities, or an unknown target language — now returns 400 instead of 500. An XLIFF file that repeats a unit identifier is applied once instead of once per repeat.
- Routing: translated permalinks on non-Latin sites could be generated broken. WordPress percent-encodes non-ASCII slugs, so an ordinary 26-character Japanese title becomes a 198-character slug; when two collided, the trim that made room for a -2 suffix cut through a percent-escape. Apache and nginx reject a malformed escape in a path with a 400 before WordPress runs, so that permalink could not be opened at all. The trim is now character-aware, exactly as WordPress does it for post slugs.
- Multisite: the notice explaining that PerfLocale cannot run alongside WPML, Polylang or TranslatePress now also appears in Network Admin, where WordPress does not fire the hook it previously used — so an operator who network-activated the other plugin saw nothing at all. It now also says their translations stay in the database and can be imported after deactivating.
- Add-ons: a failed add-on boot no longer leaves a permanent warning. WordPress replaces plugin files in place during an update, so a request landing mid-write can report a parse error for a problem that fixes itself on the next request — and that was recorded permanently, shown to every administrator, with the server's absolute file paths in the message. The record now retires as soon as the add-on boots successfully, clearing a quarantine clears its boot errors too, and the paths are stripped.