{"id":297618,"date":"2026-08-31T13:13:18","date_gmt":"2026-08-31T13:13:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/perflocale\/"},"modified":"2026-08-31T13:13:04","modified_gmt":"2026-08-31T13:13:04","slug":"perflocale","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/perflocale\/","author":23074463,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.0","stable_tag":"1.0.0","tested":"7.1","requires":"6.4","requires_php":"8.1","requires_plugins":null,"header_name":"PerfLocale","header_author":"Alex Georgiev","header_description":"Performance-first multilingual plugin for WordPress. Translate posts, pages, products, taxonomies, strings, and slugs without slowing your site down.","assets_banners_color":"575761","last_updated":"2026-08-31 13:13:04","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/perflocale.com","header_author_uri":"https:\/\/alexgv.com","rating":0,"author_block_rating":0,"active_installs":0,"downloads":55,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"alexgeorgiev","date":"2026-08-31 13:13:04","revision":3674300}},"upgrade_notice":{"1.0.0":"<p>Initial release of PerfLocale.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3674295,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3674295,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3674295,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3674295,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3674295,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3676386,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"PerfLocale \\u2014 live demo\",\"description\":\"A four-language WordPress site with PerfLocale already configured: English, German, French and Italian, with translated content and one deliberate gap so you can see the fallback behaviour.\",\"author\":\"perflocale\"},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=perflocale\",\"preferredVersions\":{\"php\":\"8.2\",\"wp\":\"latest\"},\"steps\":[{\"step\":\"login\",\"username\":\"admin\",\"password\":\"password\"},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"perflocale\"},\"options\":{\"activate\":true}},{\"step\":\"setSiteOptions\",\"options\":{\"blogname\":\"Acme Bakery\",\"blogdescription\":\"Bread that takes its time\",\"permalink_structure\":\"\\\/%postname%\\\/\"}},{\"step\":\"runPHP\",\"code\":\"<?php\\nrequire_once '\\\/wordpress\\\/wp-load.php';\\n\\n\\\/*\\n * Bail cleanly rather than fatally if the plugin is not active. On the\\n * wordpress.org \\\"Live Preview\\\" the directory supplies the plugin itself, so\\n * the install step above may be redundant there; keeping it means this same\\n * file also works when opened directly from a blueprint-url, and this guard\\n * means neither path can white-screen a visitor if activation is skipped.\\n *\\\/\\nif ( ! class_exists( '\\\\\\\\PerfLocale\\\\\\\\Plugin' ) ) {\\n\\treturn;\\n}\\n\\n\\\/*\\n * Provision a four-language demo.\\n *\\n * Mirrors what `wp perflocale languages add <locale>` does, so the rows are\\n * identical to a real install: the display name, native name, flag and text\\n * direction all come from the plugin's own bundled table rather than being\\n * derived from the locale string.\\n *\\\/\\n$plugin  = \\\\PerfLocale\\\\Plugin::get_instance();\\n$repo    = $plugin->get( 'lang_repo' );\\n$presets = (array) require WP_PLUGIN_DIR . '\\\/perflocale\\\/data\\\/languages.php';\\n\\nforeach ( array( 'en_US', 'de_DE', 'fr_FR', 'it_IT' ) as $locale ) {\\n\\tif ( $repo->find_by_locale( $locale ) ) {\\n\\t\\tcontinue;\\n\\t}\\n\\n\\t$preset = null;\\n\\tforeach ( $presets as $candidate ) {\\n\\t\\tif ( is_array( $candidate ) && strcasecmp( (string) ( $candidate['locale'] ?? '' ), $locale ) === 0 ) {\\n\\t\\t\\t$preset = $candidate;\\n\\t\\t\\tbreak;\\n\\t\\t}\\n\\t}\\n\\n\\t$slug = (string) ( $preset['slug'] ?? strtolower( substr( $locale, 0, 2 ) ) );\\n\\n\\t$repo->insert(\\n\\t\\tarray(\\n\\t\\t\\t'slug'           => $slug,\\n\\t\\t\\t'locale'         => $locale,\\n\\t\\t\\t'name'           => (string) ( $preset['name'] ?? $locale ),\\n\\t\\t\\t'native_name'    => (string) ( $preset['native_name'] ?? $locale ),\\n\\t\\t\\t'flag'           => (string) ( $preset['flag'] ?? $slug ),\\n\\t\\t\\t'is_active'      => 1,\\n\\t\\t\\t\\\/*\\n\\t\\t\\t * Always insert as non-default and promote below. Writing\\n\\t\\t\\t * is_default=1 here would leave the table with TWO defaults,\\n\\t\\t\\t * because set_default() is what demotes the previous one.\\n\\t\\t\\t *\\\/\\n\\t\\t\\t'is_default'     => 0,\\n\\t\\t\\t'text_direction' => (string) ( $preset['text_direction'] ?? 'ltr' ),\\n\\t\\t)\\n\\t);\\n}\\n\\n$english = $repo->find_by_locale( 'en_US' );\\nif ( $english ) {\\n\\t$repo->set_default( (int) $english->id );\\n}\\n\\n\\\/*\\n * Translate the two posts WordPress ships with, so the dashboard has real\\n * progress to show and the front-end switcher has somewhere to switch to.\\n *\\n * Italian is deliberately left off the page: that is what makes the dashboard\\n * worth looking at, and visiting \\\/it\\\/sample-page\\\/ then demonstrates the\\n * missing-translation fallback rather than a 404.\\n *\\\/\\n$manager = new \\\\PerfLocale\\\\Translation\\\\PostTranslationManager(\\n\\t$plugin->get( 'cache' ),\\n\\t$plugin->get( 'settings' )\\n);\\n\\n$content = array(\\n\\t'hello-world' => array(\\n\\t\\t'de' => array( 'Hallo Welt!', 'Willkommen bei WordPress. Das ist dein erster Beitrag. Bearbeite oder l\\u00f6sche ihn und beginne mit dem Schreiben!' ),\\n\\t\\t'fr' => array( 'Bonjour tout le monde\\u00a0!', 'Bienvenue sur WordPress. Ceci est votre premier article. Modifiez-le ou supprimez-le, puis lancez-vous\\u00a0!' ),\\n\\t\\t'it' => array( 'Ciao mondo!', 'Benvenuto su WordPress. Questo \\u00e8 il tuo primo articolo. Modificalo o eliminalo, e inizia a scrivere!' ),\\n\\t),\\n\\t'sample-page' => array(\\n\\t\\t'de' => array( 'Beispielseite', 'Dies ist eine Beispielseite. Sie unterscheidet sich von einem Beitrag, weil sie an einer festen Stelle bleibt.' ),\\n\\t\\t'fr' => array( \\\"Page d'exemple\\\", \\\"Ceci est une page d'exemple. Elle diff\\u00e8re d'un article car elle reste au m\\u00eame endroit.\\\" ),\\n\\t),\\n);\\n\\nforeach ( $content as $source_slug => $targets ) {\\n\\t$type   = 'sample-page' === $source_slug ? 'page' : 'post';\\n\\t$source = get_page_by_path( $source_slug, OBJECT, $type );\\n\\n\\tif ( ! $source instanceof WP_Post ) {\\n\\t\\tcontinue;\\n\\t}\\n\\n\\tforeach ( $targets as $lang_slug => $strings ) {\\n\\t\\t$translation_id = $manager->create_translation( (int) $source->ID, $lang_slug, true );\\n\\n\\t\\tif ( ! $translation_id ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\n\\t\\t\\\/*\\n\\t\\t * create_translation() deliberately produces a draft, so the front end\\n\\t\\t * keeps serving the source language until a human approves the copy.\\n\\t\\t * A demo wants them live, so publish explicitly.\\n\\t\\t *\\\/\\n\\t\\twp_update_post(\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'ID'           => (int) $translation_id,\\n\\t\\t\\t\\t'post_title'   => $strings[0],\\n\\t\\t\\t\\t'post_content' => $strings[1],\\n\\t\\t\\t\\t'post_status'  => 'publish',\\n\\t\\t\\t)\\n\\t\\t);\\n\\t}\\n}\\n\\n\\\/*\\n * The language-prefixed rules (\\\/de\\\/, \\\/fr\\\/, \\\/it\\\/) are generated from the\\n * language table, which did not exist when WordPress last built its rewrite\\n * rules. Rebuild once so the front end resolves immediately.\\n *\\\/\\nflush_rewrite_rules( false );\\n\"}]}"}},"all_blocks":{"perflocale\/if-language":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"perflocale\/if-language","version":"1.0.0","title":"Show If Language","category":"theme","icon":"filter","description":"Wrap inner content so it only renders when the visitor's language matches the selected set.","keywords":["language","conditional","multilingual","perflocale"],"textdomain":"perflocale","supports":{"html":false,"align":false,"color":{"background":false,"text":false},"spacing":{"margin":false,"padding":false}},"attributes":{"languages":{"type":"array","default":[],"items":{"type":"string"}},"invert":{"type":"boolean","default":false}},"editorScript":"file:.\/index.js","editorStyle":"perflocale-block-editor"},"perflocale\/language-switcher":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"perflocale\/language-switcher","version":"1.0.0","title":"Language Switcher","category":"widgets","icon":"translation","description":"Display a language switcher for visitors to change languages.","keywords":["language","translation","multilingual","perflocale"],"textdomain":"perflocale","supports":{"html":false,"align":["left","center","right","wide","full"],"className":true,"spacing":{"padding":true,"margin":true},"color":{"background":true,"text":true},"typography":{"fontSize":true}},"attributes":{"display":{"type":"string","default":"dropdown"},"layout":{"type":"string","default":"horizontal"},"style":{"type":"string","default":"flags_names"},"nameFormat":{"type":"string","default":"native"},"triggerFormat":{"type":"string","default":"inherit"},"hideCurrent":{"type":"boolean","default":true},"showUntranslated":{"type":"boolean","default":false},"untranslatedLink":{"type":"string","default":"homepage"},"arrowStyle":{"type":"string","default":"single"},"showFlags":{"type":"boolean","default":true},"showNames":{"type":"boolean","default":true},"fontSize":{"type":"number","default":14},"flagSize":{"type":"number","default":20},"gap":{"type":"number","default":8}},"editorScript":"file:.\/index.js","style":"perflocale-frontend","blockHooks":{"core\/site-title":"after"}}},"tagged_versions":["1.0.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3674295,"resolution":"1","location":"assets","locale":"","width":1600,"height":1000},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3674295,"resolution":"2","location":"assets","locale":"","width":1600,"height":1000},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3674295,"resolution":"3","location":"assets","locale":"","width":1600,"height":1000},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3674295,"resolution":"4","location":"assets","locale":"","width":1600,"height":1000},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3674295,"resolution":"5","location":"assets","locale":"","width":1600,"height":1000},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3674295,"resolution":"6","location":"assets","locale":"","width":1600,"height":1000},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3674295,"resolution":"7","location":"assets","locale":"","width":1600,"height":1000},"screenshot-8.png":{"filename":"screenshot-8.png","revision":3674295,"resolution":"8","location":"assets","locale":"","width":1600,"height":1000},"screenshot-9.png":{"filename":"screenshot-9.png","revision":3674295,"resolution":"9","location":"assets","locale":"","width":1600,"height":1000}},"screenshots":{"1":"Front-end language switcher - block, shortcode, or template tag rendered as an accessible ARIA listbox, plus an optional append to classic theme menus","2":"WooCommerce cart in German - translated product titles, attributes, and currency","3":"PerfLocale dashboard - per-language translation progress for every post type, with draft and outdated counts","4":"Settings - Export &amp; Import: bring existing translations across from WPML, Polylang, or TranslatePress as a background job that is safe to re-run","5":"Languages screen - add languages, set the default, and mark text direction","6":"Settings - Performance: string-translation mode (files or database), object cache, and slug preloading","7":"Strings screen - translate gettext strings from any plugin or theme, with PO export and import","8":"Settings - URL &amp; Routing: missing-translation action and per-language fallback chains","9":"Jobs - bulk and whole-site translation runs in the background: chunked, resumable, with automatic retries and per-blog status"}},"plugin_section":[262246],"plugin_tags":[104,1385,1734,99,2333],"plugin_category":[48],"plugin_contributors":[232498],"plugin_business_model":[],"class_list":["post-297618","plugin","type-plugin","status-publish","hentry","plugin_section-dashboard-widgets","plugin_tags-i18n","plugin_tags-language","plugin_tags-localization","plugin_tags-multilingual","plugin_tags-translation","plugin_category-language-tools","plugin_contributors-alexgeorgiev","plugin_committers-alexgeorgiev"],"banners":{"banner":"https:\/\/ps.w.org\/perflocale\/assets\/banner-772x250.png?rev=3674295","banner_2x":"https:\/\/ps.w.org\/perflocale\/assets\/banner-1544x500.png?rev=3674295","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/perflocale\/assets\/icon.svg?rev=3674295","icon":"https:\/\/ps.w.org\/perflocale\/assets\/icon.svg?rev=3674295","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-1.png?rev=3674295","caption":"Front-end language switcher - block, shortcode, or template tag rendered as an accessible ARIA listbox, plus an optional append to classic theme menus"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-2.png?rev=3674295","caption":"WooCommerce cart in German - translated product titles, attributes, and currency"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-3.png?rev=3674295","caption":"PerfLocale dashboard - per-language translation progress for every post type, with draft and outdated counts"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-4.png?rev=3674295","caption":"Settings - Export &amp; Import: bring existing translations across from WPML, Polylang, or TranslatePress as a background job that is safe to re-run"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-5.png?rev=3674295","caption":"Languages screen - add languages, set the default, and mark text direction"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-6.png?rev=3674295","caption":"Settings - Performance: string-translation mode (files or database), object cache, and slug preloading"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-7.png?rev=3674295","caption":"Strings screen - translate gettext strings from any plugin or theme, with PO export and import"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-8.png?rev=3674295","caption":"Settings - URL &amp; Routing: missing-translation action and per-language fallback chains"},{"src":"https:\/\/ps.w.org\/perflocale\/assets\/screenshot-9.png?rev=3674295","caption":"Jobs - bulk and whole-site translation runs in the background: chunked, resumable, with automatic retries and per-blog status"}],"raw_content":"<!--section=description-->\n<p>PerfLocale is a <strong>performance-first multilingual plugin<\/strong> for WordPress. A 3-layer cache, batch-preloaded queries, and conditional hook registration keep its own code to a small fraction of total page time \u2014 a few milliseconds per page, A\/B-measured.<\/p>\n\n<h4>What you get<\/h4>\n\n<ul>\n<li><strong>Content translation<\/strong> - posts, pages, any custom post type, taxonomies, and URL slugs, with translation-status tracking<\/li>\n<li><strong>URL routing<\/strong> - subdirectory (<code>\/en\/<\/code>), subdomain (<code>en.example.com<\/code>), per-domain, or query-parameter (<code>?lang=en<\/code>) modes; auto-detect from URL, cookie, browser, or an edge\/CDN hint<\/li>\n<li><strong>String translation<\/strong> - gettext strings from any plugin or theme, file-based (<code>.l10n.php<\/code>) or database-mode, with full CLDR plural rules (Arabic 6 forms, Russian 3) and context support<\/li>\n<li><strong>Language switcher<\/strong> - block, shortcode, widget, menu, admin-bar, and template tags with full ARIA listbox accessibility<\/li>\n<li><strong>SEO<\/strong> - hreflang (HTML + HTTP) and sitemap alternates; integrates with Yoast, Rank Math, AIOSEO, SEOPress, The SEO Framework, and Slim SEO<\/li>\n<li><strong>Machine translation<\/strong> - DeepL, Google, Microsoft, LibreTranslate, a custom agency endpoint, and the WordPress 7.0 AI Client, with monthly usage caps<\/li>\n<li><strong>Translator role<\/strong> - a dedicated role with translation-only capabilities for your translation staff<\/li>\n<li><strong>E-commerce<\/strong> - WooCommerce product\/variation\/attribute translation, multi-currency (rates supplied by your own provider hook), inventory sync, localized order emails<\/li>\n<li><strong>Reliability<\/strong> - circuit breakers around every external dependency, token-guarded atomic locks, self-healing background jobs, and Site Health diagnostics<\/li>\n<\/ul>\n\n<h4>Modern SEO &amp; UX<\/h4>\n\n<p>Translation-aware features generic SEO plugins can't provide: Content-Language HTTP header, <code>data-nosnippet<\/code> guard for fallback pages, and opt-in Speculation Rules prerender and View Transitions for switching between languages. Auto-detects and integrates with 20+ plugins and themes, including WooCommerce, Elementor, ACF, Meta Box, Pods, all major SEO plugins, Gravity Forms, and the Blocksy, Kadence, and Neve themes.<\/p>\n\n<h4>For developers<\/h4>\n\n<p>200+ action\/filter hooks, a full REST API, WP-CLI commands, and a documented addon system. Every internal primitive is <code>@api<\/code> and semver-stable across 1.x. Multisite-ready. Full docs at <strong>https:\/\/perflocale.com\/docs\/<\/strong>.<\/p>\n\n<h3>Privacy<\/h3>\n\n<p>PerfLocale is privacy-first by default. No tracking, no analytics, no visitor fingerprinting.<\/p>\n\n<ul>\n<li><strong>Cookie:<\/strong> one cookie, <code>perflocale_lang<\/code>, stores only the active language slug. <code>HttpOnly<\/code>, <code>Secure<\/code> on HTTPS, <code>SameSite=Lax<\/code>, 365-day default lifetime (filterable via <code>perflocale\/cookie_lifetime<\/code>). It can be turned off entirely (see \"Cookieless mode\" below) \u2014 language routing is URL-based and works without it.<\/li>\n<li><strong>Visitor IP:<\/strong> never logged or stored. The optional GeoIP-redirect feature (disabled by default) ships with no lookup provider and no endpoint, so out of the box it sends the IP nowhere. If you wire a source yourself through the <code>perflocale\/geo\/lookup_country<\/code> or <code>perflocale\/geo\/providers<\/code> filter, the IP is passed to that source once per first visit to resolve a country code; the country code is then cached server-side (24 hours by default) under a salted, non-reversible key - an HMAC-SHA256, keyed with the site's auth salt, of the IP after <code>wp_privacy_anonymize_ip()<\/code> has zeroed the host bits - never the raw IP or any value reversible to it.<\/li>\n<li><strong>WordPress Privacy API integration:<\/strong> Tools \u2192 Export Personal Data and Tools \u2192 Erase Personal Data both work. The eraser zeroes <code>created_by<\/code> on the background jobs the data subject dispatched and deletes their per-user UI-state meta \u2014 returning <code>items_removed<\/code>\/<code>items_retained<\/code> counts. The same flow runs on the admin <code>delete_user<\/code> path. Full detail in the docs.<\/li>\n<li><strong>Consent gating:<\/strong> the <code>perflocale\/privacy\/consent_given<\/code> filter lets any consent-management plugin (Cookiebot, Complianz, OneTrust, etc.) hold back PerfLocale until a visitor has consented. When the filter returns false, the <code>perflocale_lang<\/code> cookie is not set, and the GeoIP and browser-language redirects do not run (no outbound request is made).<\/li>\n<li><strong>Cookieless mode:<\/strong> PerfLocale \u2192 Settings \u2192 URL &amp; Routing \u2192 \"Language Cookie\" turns the <code>perflocale_lang<\/code> cookie off entirely \u2014 no consent-management plugin required. URL-based language routing keeps working; you only lose \"remember my language\" on non-prefixed URLs.<\/li>\n<li><strong>Suggested privacy-policy text:<\/strong> auto-registered via <code>wp_add_privacy_policy_content()<\/code>. The sections shown adapt to which features are enabled \u2014 GeoIP wording only appears if GeoIP is on, MT wording only appears if MT is on.<\/li>\n<\/ul>\n\n<p>Full technical detail: https:\/\/perflocale.com\/docs\/privacy\/<\/p>\n\n<h3>External Services<\/h3>\n\n<p>PerfLocale can optionally connect to external services for machine translation. All external service calls are <strong>disabled by default<\/strong> and require explicit user configuration (entering an API key and enabling the feature in settings). No data is sent to any external service without your action. The hosted providers below are contacted over HTTPS; for self-hosted or user-configured endpoints (LibreTranslate, agency, webhooks) use an HTTPS URL.<\/p>\n\n<p>The GeoIP-redirect and WooCommerce exchange-rate features ship with <strong>no<\/strong> provider and contact <strong>no<\/strong> service on their own: they call nothing unless you wire a source yourself through the <code>perflocale\/geo\/lookup_country<\/code> \/ <code>perflocale\/geo\/providers<\/code> and <code>perflocale\/woocommerce\/exchange_rates_fetched<\/code> \/ <code>perflocale\/woocommerce\/exchange_rate_providers<\/code> filters. If you connect one, disclosing that service is your responsibility as the site owner.<\/p>\n\n<p>API keys for the providers below can be supplied via an environment variable, a <code>wp-config.php<\/code> constant, or the admin Settings field (see the \"Can I keep API keys out of the database?\" FAQ).<\/p>\n\n<h4>Machine Translation<\/h4>\n\n<p>When you enable machine translation and configure an API key in PerfLocale \u2192 Settings \u2192 Addons \u2192 Machine Translation, the plugin sends the text you ask it to translate to the selected provider, together with source\/target language codes and your API key. That text is: post titles, content and excerpts; taxonomy term names and descriptions; interface strings listed on the Strings screen (which can include strings registered by other plugins and themes) when you use its machine-translation controls; and, when meta translation is enabled, registered meta values such as SEO titles and descriptions or custom text fields. It is sent when you click \"Machine Translate\", run a bulk or site-wide translation, translate via WP-CLI or the REST API, or enable auto-translate on publish. Each provider below receives exactly that data, and only for the actions just listed. The provider API additionally defines a connection-test call that sends only your API key (no post content) so an add-on can verify credentials; no screen, WP-CLI command or REST route in PerfLocale itself invokes it.<\/p>\n\n<ul>\n<li><strong>DeepL<\/strong> - api.deepl.com \/ api-free.deepl.com. Commercial neural-translation API (free and paid tiers). Receives the text, language codes and API key described above.\n<a href=\"https:\/\/www.deepl.com\/en\/pro-license\">Terms of Service<\/a> | <a href=\"https:\/\/www.deepl.com\/en\/privacy\">Privacy Policy<\/a><\/li>\n<li><strong>Google Cloud Translation<\/strong> - translation.googleapis.com. Google's paid cloud translation API. Receives the text, language codes and API key described above.\n<a href=\"https:\/\/cloud.google.com\/terms\">Terms of Service<\/a> | <a href=\"https:\/\/policies.google.com\/privacy\">Privacy Policy<\/a><\/li>\n<li><strong>Microsoft Azure Translator<\/strong> - api.cognitive.microsofttranslator.com. Microsoft's paid cloud translation API. Receives the text, language codes and API key described above.\n<a href=\"https:\/\/azure.microsoft.com\/en-us\/support\/legal\/\">Terms of Service<\/a> | <a href=\"https:\/\/www.microsoft.com\/en-us\/privacy\/privacystatement\">Privacy Policy<\/a><\/li>\n<li><strong>LibreTranslate<\/strong> - self-hosted or user-configured URL. Open-source translation server you host yourself or point at an instance you trust; the plugin calls no hard-coded LibreTranslate endpoint. Receives the text and language codes described above at the URL you configure. Terms of service and privacy policy are governed by the LibreTranslate instance you configure; the AGPL-3.0 linked below is the governing license of the software itself.\n<a href=\"https:\/\/github.com\/LibreTranslate\/LibreTranslate\/blob\/main\/LICENSE\">Terms (AGPL-3.0 License)<\/a> | <a href=\"https:\/\/github.com\/LibreTranslate\/LibreTranslate\">Source Code<\/a><\/li>\n<li><strong>WordPress AI Client<\/strong> - no hard-coded endpoint; delegated to WordPress core. When you select the \"WP AI Client\" provider on WordPress 7.0+ (or a host that ships the AI Client feature plugin), PerfLocale builds a short translation prompt (the text described above plus source\/target language codes) and hands it to WordPress core's <code>wp_ai_client_prompt()<\/code> function. PerfLocale itself makes no outbound HTTP request for this provider: WordPress core (and whichever AI provider you configured under core's AI Connectors settings) performs the network call. The data sent, the destination, and the governing Terms of Service \/ Privacy Policy are therefore those of the AI provider you configured in WordPress core, only for the actions listed above.\n<a href=\"https:\/\/make.wordpress.org\/ai\/2025\/07\/17\/ai-building-blocks\/\">WordPress AI Building Blocks<\/a><\/li>\n<\/ul>\n\n<h4>External Translation Agency<\/h4>\n\n<p>When you configure an external agency URL in PerfLocale \u2192 Settings \u2192 Addons \u2192 Machine Translation, the plugin sends post content to the configured endpoint for human or agency translation:<\/p>\n\n<ul>\n<li><strong>Custom Agency Endpoint<\/strong> - user-configured URL. A webhook endpoint you configure (use an HTTPS URL) to send post content to a human translator or translation agency for offline processing; the destination is entirely under your control and the plugin calls no hard-coded endpoint. The plugin sends post text, source\/target language codes, and a unique request ID, only when you submit a translation request; the agency must return the translated text in the immediate response. Terms of service and privacy policy are governed by the agency whose endpoint you choose to configure; review their public policies before sending post content.<\/li>\n<\/ul>\n\n<h4>Webhooks<\/h4>\n\n<p>When you register webhooks via the PerfLocale REST API, the plugin sends event notifications to your configured webhook URLs when translations are created, updated, or content changes:<\/p>\n\n<ul>\n<li><strong>User-configured webhook URLs<\/strong> - registered via REST API (loopback and private-network destinations are rejected). Endpoints you register via the PerfLocale REST API to receive translation-lifecycle notifications (use HTTPS URLs); the destinations are entirely under your control and the plugin calls no hard-coded URL. The plugin POSTs the event type, translation data (post IDs, language codes, status), and a timestamp whenever a translation is created, updated, or otherwise changes, and signs each payload with HMAC-SHA256 when a shared secret is configured. Terms of service and privacy policy are governed by whatever destination you register; review the operator's public policies before registering the URL.<\/li>\n<\/ul>\n\n<p>PerfLocale can also publish a read-only public REST endpoint for edge runtimes (<code>\/wp-json\/perflocale\/v1\/config<\/code>). It is served by your own site, makes no outbound third-party request and sends no data anywhere - see the \"Does PerfLocale expose anything to edge workers?\" FAQ for the full description.<\/p>\n\n<!--section=installation-->\n<p><strong>Permalinks:<\/strong> pretty permalinks (<strong>Settings \u2192 Permalinks<\/strong> set to anything other than \"Plain\") are recommended \u2014 with them WordPress guarantees that language-prefixed URLs such as <code>\/de\/\u2026<\/code> reach WordPress on every server. Plain permalinks also work (URLs become <code>\/de\/?p=123<\/code>) as long as the server routes all paths to WordPress, which nginx configurations and Apache setups with the standard WordPress <code>.htaccess<\/code> block do. Subdomain and per-domain URL modes carry the language in the hostname and work with any permalink setting. For servers that do not route unknown paths to WordPress, query-parameter mode (URLs become <code>example.com\/page?lang=de<\/code>, selectable under <strong>PerfLocale \u2192 Settings \u2192 URL &amp; Routing<\/strong>) works with any permalink structure \u2014 including Plain \u2014 on every server, keeping clean URLs for the default language. Site Health reports the exact status for your server.<\/p>\n\n<ol>\n<li>Upload the <code>perflocale<\/code> folder to <code>\/wp-content\/plugins\/<\/code><\/li>\n<li>Activate PerfLocale through the <strong>Plugins<\/strong> menu in WordPress<\/li>\n<li>Go to <strong>PerfLocale \u2192 Languages<\/strong> and add your languages<\/li>\n<li>Set one language as the default<\/li>\n<li>Go to <strong>PerfLocale \u2192 Settings \u2192 URL &amp; Routing<\/strong> and choose your URL structure (subdirectory, subdomain, per-language domain, or query parameter)<\/li>\n<li>Start translating - click the language badges next to any post, page, or term to create translations<\/li>\n<\/ol>\n\n<h4>Quick Start<\/h4>\n\n<ol>\n<li><strong>Add languages<\/strong>: Go to PerfLocale \u2192 Languages. Add English as default, then add Bulgarian, German, etc.<\/li>\n<li><strong>Translate a post<\/strong>: Edit any post. In the PerfLocale meta box, click a language badge to create a translation. Edit the translation in the standard WordPress editor.<\/li>\n<li><strong>Add the language switcher<\/strong>: Add the \"Language Switcher\" block to any template or page, drop the Language Switcher widget into a widget area, or use the <code>[perflocale_switcher]<\/code> shortcode. To append it to a classic theme menu, tick the menu location under <strong>PerfLocale \u2192 Settings \u2192 Language Switcher \u2192 Append to classic menus<\/strong> (developers can also control it with the <code>perflocale\/switcher\/add_to_menu<\/code> filter).<\/li>\n<li><strong>Configure SEO<\/strong>: Go to PerfLocale \u2192 Settings \u2192 SEO. Enable hreflang tags and select your SEO plugin for automatic integration.<\/li>\n<li><strong>Set up machine translation<\/strong> (optional): Enable Machine Translation on the PerfLocale \u2192 Addons screen, then go to PerfLocale \u2192 Settings \u2192 Addons \u2192 Machine Translation. Enter your API key for DeepL, Google, or Microsoft, and enable auto-translate on publish. For production \/ staging deployments you can also supply API keys via environment variables (e.g. <code>PERFLOCALE_DEEPL_API_KEY<\/code>) or <code>wp-config.php<\/code> constants of the same name \u2014 env wins over constant wins over the database value, matching WordPress 7.0's AI Connectors source-priority pattern. See the API Keys documentation page for the full list of supported names.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20perflocale%20slow%20down%20my%20site%3F\"><h3>Does PerfLocale slow down my site?<\/h3><\/dt>\n<dd><p>Very little \u2014 performance is a core design goal. The plugin's own code stays a small fraction of total page time, typically a few milliseconds per page, with three layers of caching so most requests never touch the database. Larger sites \u2014 many translated posts across many languages \u2014 benefit from running a persistent object cache like Redis. PerfLocale is fully compatible with page-cache plugins (WP Super Cache, W3 Total Cache, LiteSpeed Cache, etc.); cached pages already include the translated output from the request that filled the cache. Real-world page speed depends mostly on your theme, hosting, and other plugins.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20woocommerce%3F\"><h3>Does it work with WooCommerce?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale includes a deep WooCommerce integration: translate products, variations, categories, and attributes. Inventory (stock, SKU, weight, dimensions) syncs automatically across language variants. Multi-currency support, with exchange rates supplied by a provider your site registers via filter. Order emails are sent in the customer's language. The mini-cart, cart, and checkout all display correctly in every language.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20page%20builders%3F\"><h3>Does it work with page builders?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale integrates with Elementor, Beaver Builder, Bricks Builder, Oxygen Classic, and Oxygen 6.0. Each builder's content is registered as translatable meta, and dedicated Language Switcher widgets\/elements are provided for Elementor, Beaver Builder, and Bricks \u2014 in Oxygen (Classic or 6.0) use the <code>[perflocale_switcher]<\/code> shortcode in a Code Block or Shortcode element.<\/p><\/dd>\n<dt id=\"can%20i%20migrate%20from%20wpml%2C%20polylang%2C%20or%20translatepress%3F\"><h3>Can I migrate from WPML, Polylang, or TranslatePress?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale includes built-in migration tools for all three plugins. Go to PerfLocale \u2192 Settings \u2192 Export &amp; Import and use the Migration section. All migrations run in batches with transaction safety - if anything fails, your data is rolled back.<\/p><\/dd>\n<dt id=\"does%20it%20support%20rtl%20languages%3F\"><h3>Does it support RTL languages?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale detects the text direction from the language configuration and sets the correct <code>dir=\"rtl\"<\/code> attribute on the HTML element. The language switcher and admin UI work correctly with RTL languages like Arabic and Hebrew.<\/p><\/dd>\n<dt id=\"is%20it%20compatible%20with%20caching%20plugins%3F\"><h3>Is it compatible with caching plugins?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale works with all major caching plugins (WP Super Cache, W3 Total Cache, LiteSpeed Cache, WP Rocket). Each language version has its own URL, so page caches naturally separate content by language. Any response whose language was decided by something other than the URL \u2014 a GeoIP or browser-language redirect, a returning visitor's language cookie \u2014 is automatically marked uncacheable (via WordPress' <code>nocache_headers()<\/code>), so one visitor's language can never be cached and served to everyone. On an edge or server cache (Varnish, nginx fastcgi_cache) make sure Cache-Control is honoured or those responses are excluded. Note that enabling GeoIP \/ browser redirection makes default-language entry URLs uncacheable by design \u2014 a page cache serving them from cache would skip the redirect entirely; if you need both, use the bundled edge worker (assets\/js\/edge-helper.js) to route at the CDN instead.<\/p><\/dd>\n<dt id=\"can%20i%20use%20it%20on%20multisite%3F\"><h3>Can I use it on multisite?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale is multisite-compatible. Each site in the network has its own languages and translations. Static caches are properly scoped and reset on blog switches.<\/p><\/dd>\n<dt id=\"can%20i%20keep%20api%20keys%20out%20of%20the%20database%3F\"><h3>Can I keep API keys out of the database?<\/h3><\/dt>\n<dd><p>Yes. PerfLocale resolves every machine-translation API key from three sources in priority order: environment variable \u2192 <code>wp-config.php<\/code> constant \u2192 database setting (whichever has a non-empty value first wins). Set <code>PERFLOCALE_DEEPL_API_KEY<\/code>, <code>PERFLOCALE_GOOGLE_API_KEY<\/code>, <code>PERFLOCALE_MICROSOFT_API_KEY<\/code>, <code>PERFLOCALE_LIBRE_API_KEY<\/code>, <code>PERFLOCALE_LIBRE_URL<\/code>, <code>PERFLOCALE_AGENCY_URL<\/code> or <code>PERFLOCALE_AGENCY_API_KEY<\/code> in your container env or <code>wp-config.php<\/code> and the admin-side fields become read-only, showing which environment variable or <code>wp-config.php<\/code> constant supplies the value. Database backups, exports, and SQL dumps then never contain the secret.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20my%20translations%20if%20i%20uninstall%20the%20plugin%3F\"><h3>What happens to my translations if I uninstall the plugin?<\/h3><\/dt>\n<dd><p>By default nothing is lost: uninstalling removes the plugin's roles, capabilities, scheduled tasks, and caches, but keeps all translations, languages, and settings in the database so a later re-install picks up exactly where you left off. If you want a complete removal instead, enable \"Delete all plugin data when uninstalling\" in PerfLocale \u2192 Settings \u2192 Advanced before uninstalling \u2014 then every plugin table and option is deleted. Your posts and pages (including translated ones) are always preserved as normal WordPress content.<\/p><\/dd>\n<dt id=\"does%20perflocale%20expose%20anything%20to%20edge%20workers%3F\"><h3>Does PerfLocale expose anything to edge workers?<\/h3><\/dt>\n<dd><p>When you enable Edge Worker Integration (PerfLocale \u2192 Settings \u2192 Advanced), the plugin publishes a single public REST endpoint that edge runtimes (Cloudflare Workers, Vercel Edge, Netlify Edge, AWS Lambda@Edge) can read to pre-route visitors before the request ever hits PHP:<\/p>\n\n<ul>\n<li><code>GET \/wp-json\/perflocale\/v1\/config<\/code> - returns the minimum routing + language metadata an edge worker needs: active language slugs and locales, URL mode (subdirectory \/ subdomain \/ domain \/ query), URL prefix type, default language, hide-default-prefix flag, excluded paths, detection order, the edge-hint header name (<code>X-PerfLocale-Lang<\/code>) and cookie name. Response includes <code>Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400<\/code> plus an <code>ETag<\/code> so edges and browsers can revalidate cheaply with <code>If-None-Match<\/code> (304 on hit).<\/li>\n<\/ul>\n\n<p><strong>The response NEVER contains:<\/strong> machine-translation API keys, provider tokens, user data, internal post or term IDs, or any data that is not already observable from the rendered site (hreflang tags, language switcher, URL prefixes). That non-sensitive invariant is what justifies the public default. If you extend the payload via the <code>perflocale\/api\/config<\/code> filter, your additions must preserve this invariant.<\/p>\n\n<p>Public by default + filter-gated for restriction: site owners who want to restrict access (private staging sites, IP allowlists, Application-Password authentication, mTLS at a reverse proxy) hook the <code>perflocale\/edge_worker\/config_permission_callback<\/code> filter to return <code>false<\/code> or a <code>WP_Error<\/code> for unauthorised requests. Example:<\/p>\n\n<pre><code>add_filter( 'perflocale\/edge_worker\/config_permission_callback', fn () =&gt; current_user_can( 'manage_options' ) );\n<\/code><\/pre>\n\n<p>The plugin does not invent its own bearer-token or custom-header authentication scheme. Use WordPress' built-in primitives (Application Passwords for machine-to-machine auth, cookie + nonce for browser sessions, or a third-party JWT\/OAuth plugin) and gate via this filter \u2014 that way your edge-worker auth composes with the rest of your site's WP-REST authentication setup.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.0<\/h4>\n\n<p>Initial public release.<\/p>\n\n<p><strong>Content &amp; routing<\/strong> \u2014 Translate posts, pages, any custom post type, taxonomies, and URL slugs with per-language status tracking and configurable fallback chains. Routing in subdirectory (<code>\/en\/<\/code>), subdomain, per-language domain, or query-parameter (<code>?lang=en<\/code>) modes; language detection from URL, cookie, browser, or GeoIP; self-healing rewrite rules.<\/p>\n\n<p><strong>Strings<\/strong> \u2014 gettext translation for any plugin or theme, file-based (<code>.l10n.php<\/code>) or database mode, with a built-in scanner, MO\/PO hint lookup, and full CLDR plural rules (up to six forms \u2014 e.g. Arabic 6, Russian\/Polish 3), imported from multi-form PO files, editable per-form in the Strings screen, and preserved across a PO export \/ re-import.<\/p>\n\n<p><strong>Language switcher<\/strong> \u2014 Block, shortcode, nav-menu, admin-bar, widget, and template-tag switchers with full ARIA listbox accessibility, keyboard navigation + type-ahead, RTL-aware labels, configurable arrow\/label format, live repositioning, an opt-in setting to auto-append the switcher to selected classic-menu locations, and 40+ filters \/ CSS custom properties for theming.<\/p>\n\n<p><strong>SEO &amp; UX<\/strong> \u2014 hreflang (HTML <code>&lt;link&gt;<\/code> + HTTP header) with x-default and sitemap alternates. No hreflang-suppression filters are registered against Yoast \/ Rank Math \/ AIOSEO \/ SEOPress \/ The SEO Framework \/ Slim SEO, because none of them emit head- or header-stage hreflang of their own for PerfLocale's tags to collide with; instead the per-plugin addons add JSON-LD schema enrichment (<code>inLanguage<\/code> + <code>workTranslation<\/code>) and make each plugin's own sitemap language-aware. Plus Content-Language header, <code>data-nosnippet<\/code> fallback guard, and opt-in Speculation Rules prerender (registered through core's API on WP 6.8+, self-emitted on 6.4-6.7) and View Transitions.<\/p>\n\n<p><strong>Machine translation<\/strong> \u2014 DeepL, Google, Microsoft, LibreTranslate (and the WP 7.0 AI Client where available), with monthly usage caps and circuit breakers.<\/p>\n\n<p><strong>E-commerce<\/strong> \u2014 WooCommerce product \/ variation \/ attribute translation, multi-currency with scheduled exchange-rate sync from a rate provider your site registers via filter, inventory sync, and localized order emails.<\/p>\n\n<p><strong>Platform &amp; reliability<\/strong> \u2014 Multisite-ready (per-subsite languages and translations; chunked network activation), 3-layer caching, a background-job system with self-healing rescheduling, token-guarded atomic locks, circuit breakers around every external dependency, and Site Health diagnostics. Built-in batched, transaction-safe migration from WPML, Polylang, and TranslatePress.<\/p>\n\n<p><strong>Developers<\/strong> \u2014 200+ action\/filter hooks, a full REST API, WP-CLI commands, a documented addon system, API keys resolvable from environment variable \/ <code>wp-config.php<\/code> constant \/ database, and <code>@api<\/code> semver-stable internal primitives.<\/p>\n\n<p>Full release notes: https:\/\/perflocale.com\/docs\/<\/p>","raw_excerpt":"Performance-first multilingual plugin. Translate posts, pages, products, taxonomies, strings, and slugs with 3-layer caching and 20+ integrations.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/297618","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=297618"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/alexgeorgiev"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=297618"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=297618"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=297618"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=297618"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=297618"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=297618"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}