← All releases

PerfLocale 1.0.3

— WooCommerce email language, deleted percent signs, Site Health that reaches you, webhook permissions

  • WooCommerce order emails went out in the wrong language, from four separate faults. WordPress declines to switch language - recording nothing - when the target has no installed language pack or is already active; PerfLocale restored anyway, undoing someone else’s switch and leaving the rest of the request in a language nobody chose. On a store with a pack-less language that happened on every order email. The restore also ran when the body finished, which is before WooCommerce builds the headers, the attachments and the plain-text half of a multipart email, and before wp_mail - so all of those came out wrong. A run of emails from one status change could render the second in the first one’s language. And the three notifications that go to the shop rather than the customer - new order, cancelled, failed - were rendered in the customer’s language, so a store taking orders from three countries received its own alerts in three languages; they now follow the shop language, as WooCommerce’s own emails do. Recording the language no longer forces a second full order save on checkout, and renaming a language now moves the order’s language tag in the table WooCommerce actually keeps it in - with High Performance Order Storage it was left behind, and those orders fell back to the site language.
  • An integration could silently stop loading. PerfLocale kept a note of which integrations to load, refreshed at most every twelve hours. If that note was ever written at a moment when one of the plugins it integrates with had not finished loading, the integration for that plugin stayed off until the note expired - and nothing said so: the Addons screen still showed it as active, because that badge only asks whether the other plugin is installed, not whether the integration actually started. On a site with a persistent object cache such as Redis this could leave, say, WooCommerce or WPForms content untranslated for half a day after an unlucky moment, with no error anywhere. The note has been removed and the list is now worked out fresh on each request; measured on a multisite fixture, reading the note cost about the same as simply working it out (23 microseconds against 27), and the comparison only favoured working it out further as the object cache gets slower, because reading the note is two network round trips while the check itself touches nothing outside PHP.
  • Percent signs are no longer deleted from translated titles. WordPress’s plain-text sanitiser strips every %XX sequence - right for a URL, wrong for a title: "100%25 off" was stored as "100 off", and a title of only "%20" became empty. It applied wherever a translated title or excerpt was written: XLIFF import, the translation editor, machine translation and the TranslatePress migration. The same call also stripped every HTML tag from imported excerpts, so a WooCommerce short description lost its markup on every XLIFF round trip.
  • Exports refuse to publish unreadable bytes. The JSON encoder replaced any byte it could not read as UTF-8 with a question mark - the worst option in a backup, because the damaged file encodes cleanly, passes every check, replaces the previous good one, and is only found wanting at restore time. It now stops and leaves the existing file alone. PO exports additionally report what will not survive a round trip: two strings sharing a context and source text collapse into one entry and one can never be re-imported, and a string whose stored fingerprint no longer matches its text re-imports as a new entry instead of an update. Both are counted and reported by the CLI; the file itself is unchanged.
  • Site Health checks never reached the dashboard. All 25 were registered for admin screens only, so WordPress’s weekly site-health run - which happens on cron, where no admin screen exists - never saw a single one. That weekly run is what fills the issue counter beside Tools and the "Site Health Status" panel on the dashboard, and it overwrote whatever a manual visit had left there. A site with missing database tables, no default language, or translation exports readable over the web still showed a PerfLocale issue count of zero. The checks now run in that weekly pass. The three that make a network request - the export-exposure probe, the machine-translation DNS lookup and the hreflang fetch - opt out of it, so nothing slow runs unattended, and none of the checks runs on the front end at all.
  • Several Site Health checks were reporting the wrong thing. The export-exposure check treated "could not be checked" as a passed test, filing an unverified security question inside the collapsed "Passed tests" list - worst on exactly the hosts where the exposure is most likely, because the .htaccess rule it relies on does nothing on nginx or Caddy. The database-tables check looked at one table out of nine, and usually at none because a sticky option short-circuited it, so a partial restore still read "translation storage is healthy"; it now checks all nine, names any that are missing, and does it in a single query instead of nine. Machine translation switched on with no API credential saved produced three green cards while every translation failed. The background-cron check called the recommended production setup - DISABLE_WP_CRON plus a system cron - a critical failure; it now reports on whether events are actually overdue. The hreflang check counted the x-default link as if it were a language and counted the same language twice when both output placements were enabled, and it sat permanently orange on any site that was still being translated. The exchange-rate card is now critical, not a suggestion, when foreign-currency prices are being charged at the base-currency figure. Nothing counted failed background jobs, so a pipeline failing every job - a revoked machine-translation key is the usual cause - read green everywhere for a week; failures in the last 24 hours are now reported.
  • Machine-translation usage no longer turns the panel red before anything has failed. Reaching 95% of a limit you set yourself was reported as critical while a completely broken translation pipeline was reported as a suggestion. Red is now reserved for the point where requests are actually being refused.
  • Support reports say more. The Site Health information tab now records which optional PHP extensions are loaded, so a formatting or import question can be answered from the report itself. Oxygen 6 was never detected because the check looked for a constant that plugin does not define, and the Slim SEO integration reported itself inactive on every site running Slim SEO 4 - the class it looked for was renamed in that version.
  • Failed webhook deliveries are visible. PerfLocale has always written a log of deliveries it gave up on, and nothing in the plugin ever read it - there is no webhooks admin screen - so an abandoned delivery vanished in silence. A Site Health card now reports deliveries abandoned in the last 24 hours. Separately, a delivery whose host could not be resolved used to be dropped with no retry, no failure-log entry and no signal to the circuit breaker; a passing DNS failure was enough to eat a customer’s event. An unresolvable host is now retried on the same schedule as a failed request, and a genuinely unsafe address is recorded once so it is visible.
  • Registering a webhook on a multisite network now requires network-administrator permissions. Registering one asks the site to make an outbound HTTP request to an address the caller chose. On a single site that needs manage_options, which already allows installing plugins and running arbitrary code, so nothing there changes. On multisite a site administrator holds manage_options without being able to install plugins, so multisite was the one configuration where this crossed a privilege boundary. Listing and deleting webhooks are unchanged, so a site administrator can still audit and remove whatever points at their site. This is a breaking change for any network that deliberately delegated webhook registration to site administrators; the perflocale/webhooks/register_capability filter restores the previous rule.
  • Webhook registration now refuses the URLs WordPress itself refuses at send time - a port outside 80, 443 and 8080, or credentials embedded in the URL - instead of accepting them, returning a signing secret, and then failing every delivery attempt afterwards. The port list is read through WordPress’s own http_allowed_safe_ports filter, so a site that widens it for WordPress widens it here too. A refusal now names the rule that was broken rather than always reporting a private address, which was untrue for most of them.
  • Domain matching. A Host header cased differently from the stored domain fell through to the default language, and a domain with non-ASCII characters never matched at all, because a browser sends it in a different form than the one typed. Both are canonicalised before comparison using the punycode encoder WordPress itself ships rather than an optional PHP extension - deliberately, because the two encoders disagree and using the optional one would make the language a visitor gets depend on which extensions the host happens to have installed. The multisite language-host check now asks the same question the router asks, instead of a slightly different one.
  • Number formatting could stop the page on PHP 8.4. Two languages the plugin ships are not recognised by the formatter PHP 8.4 uses, and the error was not caught. It now falls back, and the fallback stopped discarding decimals.
  • An empty language switcher no longer draws an empty box. With "hide current language" on and untranslated languages hidden, an untranslated page skips every entry and the switcher has nothing to show - but its containers were drawn anyway: the floating switcher left a blank panel fixed over the corner of the page, and the widget drew its title and border around nothing. Both now render nothing. Present since 1.0.0.
  • The admin was not usable on a touchscreen. Languages could only be reordered by drag-and-drop, which does not exist on touch, and the handle was invisible until hover; below 782px each row now shows up and down buttons. The Translations list broke into unlabelled fragments on a phone; rows are rows again, with the checkbox and title pinned while the language columns scroll, and the Strings and dashboard tables pin their first column the same way. The Settings tab strip overflowed a tablet by about 220px. Every admin screen has been checked at three widths and touch targets enlarged.
  • An integration that is installed but not running is now visible. An addon skipped because it reported itself incompatible, or because it needs a newer PerfLocale than the one installed, was passed over in silence: the Addons page still showed it as active and Site Health still reported every addon booting fine. Also: creating a language now validates its slug the way editing one always did.