Hooks Reference
Developer reference for all apply_filters and do_action hooks in PerfLocale.
Filters
Settings & Configuration
perflocale/translatable_post_types
Filter the list of translatable post types.
add_filter( 'perflocale/translatable_post_types', function ( array $post_types ): array {
$post_types[] = 'product'; // Add WooCommerce products.
return $post_types;
} );Parameters: array $post_types - Array of post type slugs.
File: src/Settings.php
perflocale/translatable_taxonomies
Filter the list of translatable taxonomies.
add_filter( 'perflocale/translatable_taxonomies', function ( array $taxonomies ): array {
$taxonomies[] = 'product_cat';
return $taxonomies;
} );Parameters: array $taxonomies - Array of taxonomy slugs.
File: src/Settings.php
perflocale/excluded_paths
Filter URL paths excluded from language prefix injection.
add_filter( 'perflocale/excluded_paths', function ( array $paths ): array {
$paths[] = '/my-api/'; // Custom REST endpoint.
$paths[] = '/sitemap.xml'; // Sitemap file.
return $paths;
} );Parameters: array $paths - Array of path prefixes (e.g., '/wp-json/').
File: src/Settings.php
perflocale/cookie_lifetime
Filter the language cookie lifetime in days.
add_filter( 'perflocale/cookie_lifetime', function ( int $days ): int {
return 30; // 30 days instead of default 365.
} );Parameters: int $days - Cookie lifetime in days.
File: src/Router/LanguageRouter.php
perflocale/active_languages
Filter the list of active languages returned by the router.
// Hide a language on specific pages.
add_filter( 'perflocale/active_languages', function ( array $languages ): array {
if ( is_page( 'internal-only' ) ) {
return array_filter( $languages, fn( $l ) => $l->slug !== 'de' );
}
return $languages;
} );Parameters: array $languages - Array of language objects.
File: src/Router/LanguageRouter.php
perflocale/translatable_options
Filter the list of WordPress options that have per-language values.
Parameters: array $options - Array of option names.
File: src/Cache/CacheInvalidator.php
perflocale/date_format
Filter the date format that perflocale()->date_format() resolves for a given language. Default is the language row’s date_format field if set, else the WP date_format option. Lets a theme/addon override per-language without saving to the language row.
add_filter( 'perflocale/date_format', function ( string $format, ?object $lang ): string {
if ( $lang && $lang->slug === 'ja' ) {
return 'Y年n月j日'; // Japanese long date.
}
return $format;
}, 10, 2 );Parameters: string $format, object|null $lang.
File: src/Helper.php
perflocale/time_format
Same shape as perflocale/date_format, applied to the time format resolution path (perflocale()->time_format()).
Parameters: string $format, object|null $lang.
File: src/Helper.php
Translation Creation
perflocale/translation/post_data
Filter the new post data before a translation is created via wp_insert_post().
add_filter( 'perflocale/translation/post_data', function ( array $data, int $source_id, string $target_slug ): array {
$data['post_author'] = get_current_user_id(); // Set current user as author.
return $data;
}, 10, 3 );Parameters:
array $data- Post data array (post_type,post_status,post_title, etc.).int $source_id- Source post ID.string $target_slug- Target language slug.
File: src/Translation/PostTranslationManager.php
perflocale/translation/excluded_meta_keys
Filter the meta keys excluded from being copied when creating a translation.
add_filter( 'perflocale/translation/excluded_meta_keys', function ( array $keys, int $source_id ): array {
$keys[] = '_my_private_meta'; // Don't copy this to translations.
return $keys;
}, 10, 2 );Parameters:
array $keys- Meta key names to exclude.int $source_id- Source post ID.
File: src/Translation/PostTranslationManager.php
perflocale/translation/create_lock_ttl
Lock TTL (seconds) for the create_translation() critical section that serialises concurrent translation creation for the SAME (source_post, target_language) pair. The default of 60s covers most real-world inserts including a meta + featured-image copy step that runs synchronously. Sites with slow save_post integrations (heavy SEO plugins, image regeneration on insert) can extend this so a legitimately-long insert doesn't lose its lock mid-flight.
// Bump to 5 minutes if your save_post chain is heavy (image regen, SEO, ACF Field Sync).
add_filter( 'perflocale/translation/create_lock_ttl', static fn(): int => 300 );Parameters: int $ttl - Lock TTL in seconds. Default 60. Floor 5.
File: src/Translation/PostTranslationManager.php
perflocale/translation/create_term_lock_ttl
Same as perflocale/translation/create_lock_ttl but for taxonomy term translations. Default is shorter (30s) because terms have no meta/image copy step.
add_filter( 'perflocale/translation/create_term_lock_ttl', static fn(): int => 60 );Parameters: int $ttl - Lock TTL in seconds. Default 30. Floor 5.
File: src/Translation/TermTranslationManager.php
Content Sync
perflocale/sync_fields
Filter the fields synchronized across translations when a post is saved.
// Add custom meta keys to sync programmatically.
add_filter( 'perflocale/sync_fields', function ( array $fields, string $post_type ): array {
if ( $post_type === 'product' ) {
$fields[] = '_price';
$fields[] = '_sku';
$fields[] = '_stock_status';
}
return $fields;
}, 10, 2 );Parameters:
array $fields- Field names (built-in keys likefeatured_image,menu_order, or custom meta keys).string $post_type- The post type being saved.
Built-in field keys: featured_image, menu_order, post_parent, post_date, post_author, comment_status, ping_status. Any other value is treated as a meta key and synced via get_post_meta() / update_post_meta().
File: src/Translation/ContentSync.php
perflocale/translatable_meta_keys
Add meta keys to the auto-translated set without requiring the user to enter each one in Settings → Translation. Bundled addons (Yoast, ACF, page builders) use this hook to register their own meta keys; your custom plugin can do the same.
add_filter( 'perflocale/translatable_meta_keys', function ( array $keys, string $post_type ): array {
if ( $post_type === 'job_listing' ) {
$keys[] = '_job_description';
$keys[] = '_job_location';
}
return $keys;
}, 10, 2 );Parameters: array $keys, string $post_type (may be empty for "any post type").
File: src/Settings.php
perflocale/media/show_featured_image_panel
Whether the per-language Featured Image metabox is registered on the post-edit screen for a given post type. Default true. Returning false suppresses the metabox without disabling the frontend featured-image swap (existing per-language overrides keep working) — useful for themes or addons that render their own featured-image surface and want to host the language overrides inside that UI instead. Only consulted for post types that support thumbnails and when the active theme declares post-thumbnails support.
// Hide PerfLocale's metabox for products — a custom UI hosts the overrides.
add_filter( 'perflocale/media/show_featured_image_panel', static function ( bool $show, string $post_type ): bool {
return $post_type === 'product' ? false : $show;
}, 10, 2 );Parameters:
bool $show—trueby default.string $post_type— the post type being evaluated.
Returns: bool.
File: src/Translation/MediaTranslationManager.php
URL Handling
perflocale/router/bot_ua_pattern
Override the regex used to identify search-engine bots / crawlers. Bots that match are exempted from cookie-based language detection and from automatic redirect to the user’s preferred language - they always see the URL they crawled. Default pattern matches the case-insensitive, word-bounded tokens bot, crawl, spider, slurp, mediapartners, googlebot, bingbot, yandex, baidu — broad enough to cover the major engines and most generic crawlers. Return an empty string to disable bot exemption entirely.
add_filter( 'perflocale/router/bot_ua_pattern', function ( string $pattern ): string {
// Add a custom monitoring crawler to the bypass list.
return '/(MyMonitor|' . substr( $pattern, 2, -2 ) . ')/i';
} );Parameters: string $pattern - Compiled regex (with delimiters and flags).
File: src/Router/LanguageRouter.php
perflocale/url/convert
Filter a URL after it has been converted to a target language.
add_filter( 'perflocale/url/convert', function ( string $url, string $target_slug, string $current_slug ): string {
// Custom URL transformation.
return $url;
}, 10, 3 );Parameters:
string $url- The converted URL.string $target_slug- Target language slug.string $current_slug- Current language slug.
File: src/Router/UrlConverter.php
perflocale/redirect_default_to_prefix
When “Hide URL prefix for default language” is unchecked, the plugin 301-redirects bare URLs (e.g. / or /contact/) to the prefixed form (/en/, /en/contact/) to avoid duplicate content. Return false to disable that redirect and let both forms resolve.
add_filter( 'perflocale/redirect_default_to_prefix', '__return_false' );Parameters: bool $enabled - default true.
File: src/Router/LanguageRouter.php
SEO
perflocale/seo/hreflang_tags
Filter the full hreflang tag array (including the x-default entry) right before it’s emitted in <head> and on the Link HTTP header. Use it to add, remove, reorder, or mutate entries.
// Remove the x-default tag entirely.
add_filter( 'perflocale/seo/hreflang_tags', function ( array $tags ): array {
return array_filter( $tags, fn( $t ) => $t['hreflang'] !== 'x-default' );
} );
// Add a regional alternate that PerfLocale doesn't manage itself.
add_filter( 'perflocale/seo/hreflang_tags', function ( array $tags ): array {
$tags[] = [ 'hreflang' => 'en-ca', 'href' => 'https://ca.example.com/' ];
return $tags;
} );Parameters: array $tags - array of ['hreflang' => string, 'href' => string], computed per-request and cached.
File: src/Frontend/HreflangTags.php
Dev-mode guard: if a filter callback returns a non-array, PerfLocale emits a _doing_it_wrong() notice and falls back to the unfiltered tag list — hreflang output is preserved.
perflocale/seo/x_default_url
Filter just the URL used for the x-default entry. Cleaner than walking the whole tag array when you only want to redirect search engines to a language-picker landing page or a specific regional variant. Return an empty string to suppress the x-default entry for the current request.
// Point x-default at a language-picker landing page.
add_filter( 'perflocale/seo/x_default_url', fn() => home_url( '/languages/' ) );
// Only override for the homepage; fall back to PerfLocale's default elsewhere.
add_filter( 'perflocale/seo/x_default_url', function ( string $url ): string {
return is_front_page() ? home_url( '/choose-region/' ) : $url;
} );Parameters:
string $default_url- URL currently slated for x-default (PerfLocale’s computed value).object $default- default-language object (slug, locale, name, etc.).string $current_slug- slug of the language currently being rendered.
File: src/Frontend/HreflangTags.php
perflocale/seo/hreflang_include_fallbacks
Override the Include Fallback Languages setting programmatically. When the filter returns true, hreflang emits a tag for every active language - even those without an explicit translation of the current post - as long as the language URL would render 200 (i.e. missing_translation_action is show_default or the language has a non-empty fallback chain). Filter value wins over the admin toggle, so addon or theme code can force the inclusive mode without touching settings.
// Always advertise every active language, regardless of admin setting.
add_filter( 'perflocale/seo/hreflang_include_fallbacks', '__return_true' );
// Turn on only for singular posts with an SEO-friendly post status.
add_filter( 'perflocale/seo/hreflang_include_fallbacks', function ( bool $enabled ): bool {
if ( is_singular() && 'publish' === get_post_status() ) {
return true;
}
return $enabled;
} );Parameters: bool $enabled - current setting value (defaults to false).
Since: 1.0.0
File: src/Frontend/HreflangTags.php
perflocale/sitemap/inject_into_core
Decide whether PerfLocale injects xhtml:link hreflang alternates into the WordPress core sitemap (/wp-sitemap.xml) on this request. The default decision is computed from the seo_sitemap_source setting:
core— always inject into WP core sitemap, even when an SEO plugin is also active.plugin— never inject into WP core (let the SEO plugin's addon handle alternates exclusively in its own native sitemap).auto(default) — skip core when an SEO plugin (Yoast / Rank Math / AIOSEO) is detected via setting or runtime constant/class. Otherwise inject into core.
This filter lets sites with unusual sitemap setups (e.g. a third-party sitemap plugin PerfLocale doesn't recognise, or a Yoast sitemap that has been disabled at the Yoast settings level) override the auto-detection.
// Force injection into WP core even though Yoast is active (sitemap disabled in Yoast).
add_filter( 'perflocale/sitemap/inject_into_core', '__return_true' );
// Skip core sitemap injection unconditionally (custom sitemap plugin handles alternates).
add_filter( 'perflocale/sitemap/inject_into_core', '__return_false' );Parameters: bool $decision - the pre-filter decision derived from the seo_sitemap_source setting.
File: src/Seo/SitemapIntegration.php
Modern SEO & UX (1.0.0)
perflocale/content_language/value
Filter the BCP-47 locale emitted in the Content-Language HTTP response header. Default is the current language’s locale field normalised to hyphen-form (e.g. de_DE → de-DE); return an empty string to suppress the header on this request.
add_filter( 'perflocale/content_language/value', function ( string $locale, $current_lang ): string {
// Force the generic region-less code on specific templates.
if ( is_page( 'global-press' ) ) {
return 'en';
}
return $locale;
}, 10, 2 );Parameters: string $locale, object $current_lang - the language object from the router.
File: src/Frontend/ContentLanguageHeader.php
perflocale/view_transitions/css
Filter the CSS block emitted for the cross-document View Transitions opt-in. The default block opts in to automatic navigation transitions with a 240ms crossfade AND includes a @media (prefers-reduced-motion: reduce) guard that zeros the animation duration for users with the OS-level reduced-motion preference (navigation still completes instantly - only the crossfade is suppressed). Returning '' disables output entirely. If you override the default, keep the reduced-motion guard in your replacement CSS to preserve WCAG 2.1 SC 2.3.3 compliance.
// Add named transitions so the header and main content morph across pages.
add_filter( 'perflocale/view_transitions/css', function ( string $css ): string {
return $css . 'header.site-header { view-transition-name: site-header; }
main { view-transition-name: main-content; }';
} );Parameters: string $css - default CSS block (includes @view-transition, the 240ms crossfade rules, and the prefers-reduced-motion guard).
File: src/Frontend/ViewTransitionsEmitter.php
perflocale/view_transitions/should_emit
Short-circuit the View Transitions emitter on specific templates. Useful when a theme runs its own GSAP/slider animations on navigation that would fight the browser transition.
add_filter( 'perflocale/view_transitions/should_emit', function ( bool $should ): bool {
if ( is_singular( 'portfolio' ) ) {
return false; // GSAP page transitions are active on portfolio items.
}
return $should;
} );Parameters: bool $should_emit - defaults to true when the setting is on.
File: src/Frontend/ViewTransitionsEmitter.php
perflocale/prerender/rules
Fallback path only (WP 6.4–6.7). Filter the Speculation Rules JSON payload before emission. On WP 6.8+ use Core’s wp_load_speculation_rules action instead; our rule is registered via that API and is no longer filterable through this hook.
add_filter( 'perflocale/prerender/rules', function ( array $rules, array $urls ): array {
// Switch to prefetch instead of prerender on this site.
if ( isset( $rules['prerender'] ) ) {
$rules = [ 'prefetch' => $rules['prerender'] ];
}
return $rules;
}, 10, 2 );Parameters: array $rules, array $urls.
File: src/Frontend/SpeculationRulesEmitter.php
perflocale/prerender/should_emit
Short-circuit the Speculation Rules emitter on specific templates (applies to BOTH the WP 6.8+ Core API path and the WP < 6.8 fallback). Useful for pages where prerendering the switcher target would waste bandwidth - paginated archives with heavy media, or URLs that trigger non-idempotent GET side effects.
add_filter( 'perflocale/prerender/should_emit', function ( bool $should ): bool {
if ( is_page_template( 'templates/heavy-media-gallery.php' ) ) {
return false;
}
return $should;
} );Parameters: bool $should_emit.
File: src/Frontend/SpeculationRulesEmitter.php
perflocale/prerender/use_core_api
On WP 6.8+ PerfLocale registers its prerender rule via Core’s native Speculation Rules API (wp_load_speculation_rules action), so our rule lands inside Core’s single output script. Return false from this filter to force the fallback self-emit path instead - a standalone <script type="speculationrules" id="perflocale-speculationrules"> emitted at wp_footer priority 20. Primarily useful for integrators who want the perflocale/prerender/rules filter (fallback-path only) to remain active on modern WP, or for test environments exercising the fallback code without downgrading WordPress.
add_filter( 'perflocale/prerender/use_core_api', '__return_false' );The same path can be forced globally by defining the PERFLOCALE_FORCE_SR_FALLBACK constant - the filter is a per-request escape hatch, the constant is a site-wide switch.
Parameters: bool $use_core - defaults to true when \WP_Speculation_Rules exists.
File: src/Frontend/SpeculationRulesEmitter.php
perflocale/fallback/wrap_nosnippet
Filter whether to wrap singular-post content with <div data-nosnippet> when the visitor is viewing a non-default-language URL that’s showing the default-language post as fallback (missing_translation_action = show_default). Fires only when fallback is already detected; use this to exempt specific post types or IDs. The guard applies to both explicitly-linked default-language posts AND posts that have no translation-link entry (treated as default-language, matching the plugin-wide convention), so pages authored before PerfLocale was installed are protected too.
add_filter( 'perflocale/fallback/wrap_nosnippet', function ( bool $wrap, int $post_id ): bool {
$post = get_post( $post_id );
if ( $post && $post->post_type === 'product' ) {
return false; // Let product descriptions through - SKUs are language-agnostic.
}
return $wrap;
}, 10, 2 );Parameters: bool $wrap (default true), int $post_id.
File: src/Translation/FallbackSnippetGuard.php
perflocale/fallback/nosnippet_tag
Customise the HTML tag used to wrap fallback content. Default is div; use span for themes that render the content in an inline context where a block-level wrapper breaks layout. Must match /^[a-z]{1,8}$/ - invalid values fall back to div.
add_filter( 'perflocale/fallback/nosnippet_tag', fn( $tag ) => 'span' );Parameters: string $tag, int $post_id.
File: src/Translation/FallbackSnippetGuard.php
perflocale/indexnow/should_push
Veto an IndexNow push on a case-by-case basis. Use to skip preview/staging URLs, or to limit pushes to a rate-controlled window.
add_filter( 'perflocale/indexnow/should_push', function ( bool $should, array $urls, array $context ): bool {
// Stop all pushes outside business hours.
$hour = (int) wp_date( 'G' );
return $hour >= 9 && $hour <= 17 ? $should : false;
}, 10, 3 );Parameters: bool $should, array $urls, array $context (may contain trigger_post_id).
File: src/Seo/IndexNowPusher.php
perflocale/indexnow/urls
Modify the URL list before it’s sent. Add or remove URLs on the fly - useful for integrations with other plugins that know about URLs PerfLocale doesn’t.
add_filter( 'perflocale/indexnow/urls', function ( array $urls ): array {
// Also ping the translated AMP versions.
$amp = array_map( fn( $u ) => trailingslashit( $u ) . 'amp/', $urls );
return array_merge( $urls, $amp );
} );Parameters: array $urls, array $context.
File: src/Seo/IndexNowPusher.php
perflocale/indexnow/endpoint
Override the IndexNow endpoint per-host. Defaults to Cloudflare’s relay (https://api.indexnow.org/indexnow, which covers Google + Bing + Yandex) when the Cloudflare-relay setting is on, or direct Bing otherwise. You can force a different endpoint for specific hosts.
add_filter( 'perflocale/indexnow/endpoint', function ( string $endpoint, string $host ): string {
// Send .cn hosts to Yandex directly.
if ( str_ends_with( $host, '.cn' ) ) {
return 'https://yandex.com/indexnow';
}
return $endpoint;
}, 10, 2 );Parameters: string $endpoint, string $host, array $urls.
File: src/Seo/IndexNowPusher.php
perflocale/indexnow/key
Override the auto-generated 32-character hex key. The key is served by the plugin at {host}/{key}.txt for search-engine ownership verification - if you supply your own, make sure it’s also accessible at that URL. Rarely needed.
add_filter( 'perflocale/indexnow/key', fn() => defined( 'MY_INDEXNOW_KEY' ) ? MY_INDEXNOW_KEY : '' );Parameters: string $key.
File: src/Seo/IndexNowPusher.php
Core filter - also honoured on WP < 6.8
The WordPress Core filter wp_speculation_rules_href_exclude_paths is automatically respected by PerfLocale on WP 6.8+ (Core applies it). On older WP (6.4–6.7), the fallback path also fires this filter so exclusions like /cart/* or /checkout/* work consistently across versions.
// Exclude action URLs from speculative prerendering.
add_filter( 'wp_speculation_rules_href_exclude_paths', function ( array $paths, string $mode ): array {
if ( $mode === 'prerender' ) {
$paths[] = '/add-to-cart/*';
$paths[] = '/wishlist-toggle/*';
}
return $paths;
}, 10, 2 );Parameters: array $paths, string $mode (prefetch or prerender).
File: WordPress Core 6.8+ | src/Frontend/SpeculationRulesEmitter.php (fallback path only).
Language Switcher
perflocale/switcher/languages
Filter the languages available in the switcher.
Parameters: array $languages - Array of language objects.
File: src/Frontend/LanguageSwitcher.php
perflocale/switcher/output
Filter the final HTML output of the language switcher.
Parameters:
string $html- Rendered HTML.array $args- Switcher arguments.
File: src/Frontend/LanguageSwitcher.php
Dev-mode guard: a non-string return triggers _doing_it_wrong() and the renderer falls back to the unfiltered HTML.
perflocale/switcher/link_attrs
Filter the HTML attributes for each per-language link in the switcher - inline, dropdown, list, and flags renderers alike. Add analytics data-*, inject rel / title, override hreflang / lang / dir, or append additional CSS classes without forking the template.
// Add GA4 + Hotjar data attributes to every switcher link.
add_filter( 'perflocale/switcher/link_attrs', function ( array $attrs, object $lang, string $current_slug, array $args ): array {
$attrs['data-lang'] = $lang->slug;
$attrs['data-gtm-event'] = 'language_switch';
$attrs['data-hj-track'] = 'lang-switcher';
return $attrs;
}, 10, 4 );
// Add rel="alternate" for better SEO signal + hreflang override.
add_filter( 'perflocale/switcher/link_attrs', function ( array $attrs, object $lang ): array {
$attrs['rel'] = 'alternate';
$attrs['hreflang'] = $lang->locale ? str_replace( '_', '-', strtolower( $lang->locale ) ) : $lang->slug;
return $attrs;
}, 10, 2 );Values are escaped via esc_attr() (or esc_url() for href). Boolean true emits an HTML boolean attribute; false / null omits the attribute entirely. Attribute names are sanitized to [A-Za-z0-9:_-].
Base attributes the switcher already sets on each option (the filter is your chance to add to or override them):
href- Target translation URL.class- Per-renderer (perflocale-dd__option,perflocale-switcher-block__item, …).hreflang- Target language slug.lang- Target language slug. Lets screen readers pronounce "Français" / "العربية" with the right phonemes since the option label is in the target language, not the page language.dir-rtlwhen the target language'stext_directionis RTL; omitted otherwise. Lets RTL labels render right-to-left even on an LTR page.role,tabindex,aria-selected- Set only on dropdown options for listbox semantics.
Parameters:
array $attrs- Attribute name => value pairs.object $lang- Language object for this link (slug,name,native_name,locale,flag,text_direction).string $current_slug- Currently active language slug.array $args- Switcher arguments (template / display / layout / className …).
Args: 4
File: src/Frontend/LanguageSwitcher.php
Dev-mode guard: a non-array return triggers _doing_it_wrong() and the renderer falls back to the built-in attribute map for that option.
perflocale/switcher/arrow_html
Override the chevron icon rendered inside the dropdown trigger button. Three built-in styles ship — single (one down chevron, the classic native-select look, default), double (stacked up + down chevrons), none (empty string). The filter fires AFTER the built-in lookup so it can substitute markup for any style key, including none — useful for themes that want to inject an icon even when the user picked "no icon" in Settings → Switcher.
// Use a Font Awesome icon instead of the inline SVG chevron.
add_filter( 'perflocale/switcher/arrow_html', function ( string $html, string $style ): string {
return '<i class="fa-solid fa-chevron-down" aria-hidden="true"></i>';
}, 10, 2 );
// Use a brand SVG, but only when the user picked the "double" style.
add_filter( 'perflocale/switcher/arrow_html', function ( string $html, string $style ): string {
if ( $style !== 'double' ) { return $html; }
return '<svg viewBox="0 0 20 20" width="1em" height="1em" aria-hidden="true"><use href="#brand-double-chevron"/></svg>';
}, 10, 2 );Returned HTML is sanitized through kses_switcher() at every echo site — if your markup uses tags or attributes outside the switcher's built-in allowlist (e.g. <i> elements, data-* attributes, additional SVG tags), extend the allowlist via perflocale/switcher/kses_allowed_html below. Returning a non-string falls back to empty.
Parameters:
string $html- Computed arrow HTML for$style(empty when$styleisnoneor unknown).string $style- Resolved style key (single,double,none, or any custom keyword a theme uses as a sentinel).
Args: 2
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/panel_before
Inject HTML at the START of the dropdown panel, inside the role="listbox" container, before the first option. Useful for region groupings, search inputs, recently-used pickers, "Powered by …" notices, or any other panel chrome.
add_filter( 'perflocale/switcher/panel_before', function ( string $html, array $languages, string $current_slug, array $attrs ): string {
return '<div role="presentation" class="my-switcher-heading">' . esc_html__( 'Choose your language', 'my-theme' ) . '</div>';
}, 10, 4 );Items injected here are NOT options, so they should not carry role="option". Put non-interactive chrome inside a <div role="presentation"> so the parent listbox doesn't announce it as an option. Returned HTML is sanitized through kses_switcher() — extend the allowlist via perflocale/switcher/kses_allowed_html if your markup needs more.
Parameters:
string $html- HTML to inject (default empty).array $languages- Languages about to be rendered (objects withslug,name,native_name,locale,flag,text_direction).string $current_slug- Currently active language slug.array $attributes- Resolved switcher attributes (display / nameFormat / triggerFormat / arrowStyle / className …).
Args: 4
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/panel_after
Inject HTML at the END of the dropdown panel, inside the role="listbox" container, after the last option. Same sanitisation, parameters, and role guidance as panel_before above.
// Add a "Manage languages" link visible only to admins.
add_filter( 'perflocale/switcher/panel_after', function ( string $html ): string {
if ( ! current_user_can( 'manage_options' ) ) { return $html; }
return '<a role="presentation" href="' . esc_url( admin_url( 'admin.php?page=perflocale-languages' ) ) . '">' . esc_html__( 'Manage languages', 'my-theme' ) . '</a>';
} );Parameters: Same as panel_before.
Args: 4
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/option_content
Filter the inner HTML of each per-language switcher option — the content that sits between the wrapping <a> / <span> on every render surface (dropdown, inline, simple, list). Wraps the flag + label by default; the filter lets integrators inject region badges, mixed-direction <bdi> wraps, custom icons, script-tag indicators, etc., without forking the renderer. The same hook fires from both the dropdown and the inline / simple / list paths, so a single callback covers every option surface.
// Append a region badge to every option label.
add_filter( 'perflocale/switcher/option_content', function ( string $inner, object $lang ): string {
$region = $lang->slug === 'pt-br' ? 'Brasil'
: ( $lang->slug === 'pt' ? 'Portugal' : '' );
if ( $region === '' ) { return $inner; }
return $inner . ' <span class="lang-region">· ' . esc_html( $region ) . '</span>';
}, 10, 2 );
// Wrap the label in <bdi> for safe mixed-direction rendering.
add_filter( 'perflocale/switcher/option_content', function ( string $inner ): string {
return '<bdi>' . $inner . '</bdi>';
} );The filter return is passed through kses_fragment() (the switcher's internal kses helper, same allowlist as kses_switcher()) before being concatenated into the output, so unsafe HTML in a filter callback is stripped on every render path — including the Gutenberg block render path, which WordPress doesn't otherwise sanitize. Tags / attributes outside the built-in switcher allowlist (e.g. <bdi>, <i> icon-font glyphs, data-*) must be permitted via perflocale/switcher/kses_allowed_html.
Parameters:
string $inner- Default inner HTML (flag<span>+ label<span>, either of which may be omitted depending on theshowFlags/showNames/stylesettings).object $lang- Language object for this option (slug,name,native_name,locale,flag,text_direction).string $current_slug- Currently active language slug.array $attrs- Resolved switcher attributes (display / nameFormat / triggerFormat / arrowStyle / className …).
Args: 4
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/sanitize_output
Toggle whether kses_switcher() runs wp_kses() over the rendered HTML before echo. Default true (sanitised). Returning false skips the parse-and-rebuild pass entirely — useful on sites that fully trust their own filter callbacks and want to drop the per-render wp_kses cost.
// Skip wp_kses on this site — we trust every filter we register.
add_filter( 'perflocale/switcher/sanitize_output', '__return_false' );
// Skip wp_kses only on the frontend; keep it on for admin previews.
add_filter( 'perflocale/switcher/sanitize_output', function ( bool $sanitize ): bool {
return is_admin() ? $sanitize : false;
} );Trade-off: the renderer's per-attribute esc_url() / esc_attr() / esc_html() calls always run — they're what actually guarantee safety on baseline output. wp_kses is a defence-in-depth pass on top, useful when third-party filter callbacks (arrow_html, panel_before, panel_after, option_content, link_attrs) may not be fully trusted. Disable it when you own every callback and want maximum throughput on switcher-heavy pages.
Note: even when sanitize_output returns true, identical sanitised fragments are memoised per request by sha1( $html ), so repeated identical instances on the same page (header + footer switcher with the same languages + format) share one wp_kses call.
Parameters:
bool $sanitize- Whether to runwp_kses(defaulttrue).string $html- The HTML being considered, for context-aware decisions.
Args: 2
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/kses_allowed_html
Extend the wp_kses allowlist used to sanitize switcher HTML before addons echo it (via LanguageSwitcherBlock::kses_switcher()). Needed when a custom arrow_html, panel_before, panel_after, option_content, or link_attrs filter emits tags / attributes outside the switcher's built-in set (which covers nav, button, div, span, a, svg, path with their standard switcher attributes plus lang, dir, aria-*, tabindex, id).
// Analytics addon: allow data-tracking-id on options.
add_filter( 'perflocale/switcher/kses_allowed_html', function ( array $allowed ): array {
$allowed['a']['data-tracking-id'] = true;
return $allowed;
} );
// Theme swaps the chevron for an <i> icon-font glyph.
add_filter( 'perflocale/switcher/kses_allowed_html', function ( array $allowed ): array {
$allowed['i'] = [ 'class' => true, 'aria-hidden' => true ];
return $allowed;
} );The map follows the standard wp_kses_allowed_html shape: [ 'tag' => [ 'attr' => true, … ], … ].
Parameters:
array $allowed- Current allowlist (already merged with the post-context tags + the switcher's structural tags / attributes).string $html- The HTML being sanitized, for context-aware decisions.
Args: 2
File: src/Frontend/LanguageSwitcherBlock.php
String Translation
perflocale/string/translate
Filter a translated string before it is returned.
Parameters:
string $translated- The translated string.string $text- The original string.string $domain- Text domain.string $lang_slug- Current language slug.
File: src/String/StringTranslation.php
perflocale/strings/scanner/excluded_paths
Filter directory patterns excluded from string scanning.
Parameters: array $excluded - Array of directory patterns.
File: src/String/StringScanner.php
Dev-mode guard: a non-array return triggers _doing_it_wrong() and the scanner falls back to the built-in excluded-paths list.
perflocale/string/needs_update
Fires when a registered string's source text changes and its existing translations are marked as needing review. Useful for triggering automated re-translation, sending notifications, or logging changes.
add_action( 'perflocale/string/needs_update', function ( int $string_id, string $old_text, string $new_text, string $domain, string $context ): void {
// Example: log the change or trigger auto-translation.
error_log( "String #{$string_id} changed in {$domain}/{$context}" );
}, 10, 5 );Parameters:
int $string_id- The new string's ID.string $old_text- The previous source text that was replaced.string $new_text- The new source text.string $domain- Text domain (e.g.perflocale,woocommerce).string $context- Translation context (e.g.workflow_email_subject,email_subject_new_order).
Args: 5
File: src/Database/Repository/StringRepository.php
Machine Translation
perflocale/machine_translation/providers
Filter the registered machine translation providers.
Parameters: array $providers - Array of provider objects.
File: src/MachineTranslation/TranslationService.php
perflocale/mt/pre_translate
Filter texts before they are sent to a machine translation provider.
Parameters:
array $texts- Array of text strings.string $source_lang- Source language code.string $target_lang- Target language code.string $provider_id- Provider identifier.
File: src/MachineTranslation/TranslationService.php
perflocale/mt/post_translate
Filter translated texts after they are returned from a machine translation provider.
Parameters:
array $translated- Translated text strings.array $originals- Original text strings.string $target_lang- Target language code.string $provider_id- Provider identifier.
File: src/MachineTranslation/TranslationService.php
Dev-mode guard: returns that are not array<int,string> trigger _doing_it_wrong() and the provider response is used unfiltered.
perflocale/machine_translation/text_before_send
Filter individual text before sending to a provider.
Parameters:
string $text- Text to translate.string $provider_id- Provider identifier (e.g.,'deepl','google').string $target_lang- Target language code.
File: src/MachineTranslation/Provider/*.php
perflocale/machine_translation/result
Filter individual translation result from a provider.
Parameters:
string $translated- Translated text.string $original- Original text.string $provider_id- Provider identifier.
File: src/MachineTranslation/Provider/*.php
perflocale/mt/request_args
Filter the wp_remote_request() arguments right before an outbound call to a machine-translation provider. Use it to raise the timeout for bulk jobs, inject proxy/tracing headers, set a custom user-agent, or route calls through a corporate gateway.
// 90-second timeout + corporate proxy + custom UA for the DeepL endpoint.
add_filter( 'perflocale/mt/request_args', function ( array $args, string $url, string $provider_id ): array {
if ( $provider_id !== 'deepl' ) {
return $args;
}
$args['timeout'] = 90;
$args['user-agent'] = 'AcmeCorp-Translation/1.0';
$args['headers']['X-Forwarded-Via'] = 'corp-gw';
return $args;
}, 10, 3 );The default SSRF validation runs before this filter and cannot be bypassed: the URL is verified against the internal/private-IP blocklist first, then the filter gets to tweak args.
Parameters:
array $args-wp_remote_request()argument array (timeout, headers, body, method, sslverify …).string $url- Destination URL (already SSRF-validated).string $provider_id- Provider slug (deepl,google,microsoft,libretranslate,external_agency,wp_ai_client).
Args: 3
File: src/MachineTranslation/AbstractProvider.php
perflocale/mt/trusted_hosts
Filter the list of hostnames that skip the slow gethostbyname() step in PerfLocale's SSRF validator. Use it to register custom MT-provider hosts (for example a custom AI translator addon calling api.cohere.ai or your own self-hosted gateway) so their outbound requests don't hit the no-timeout DNS path.
// Allow a self-hosted AI gateway and Cohere's translate endpoint to skip
// the DNS step. Both must already resolve to public IPs - the localhost,
// loopback, and private-IP gates always run for IP-literal URLs and
// CANNOT be bypassed by this filter.
add_filter( 'perflocale/mt/trusted_hosts', function ( array $hosts ): array {
$hosts[] = 'api.cohere.ai';
$hosts[] = 'translate.acmecorp.internal-public.example';
return $hosts;
} );Important: this filter is a performance fast-path, not a security boundary. The localhost / loopback block and the IPv4/IPv6 private-range checks always run before anything that resolves a URL. Adding a host here only means "I trust DNS for this name to return a public IP" - it doesn't grant access to internal infrastructure. Hostnames returned by the filter are normalised (lowercased, non-strings dropped); a non-array return value falls back to the built-in list.
Default trusted hosts (built into PerfLocale): translation.googleapis.com, api.deepl.com, api-free.deepl.com, api.cognitive.microsofttranslator.com, libretranslate.com, plus api.openai.com and api.anthropic.com for AI-translator addons.
Parameters:
array<int, string> $hosts- Lowercase trusted hostnames. Add to or replace this array.
Args: 1
File: src/MachineTranslation/AbstractProvider.php
perflocale/tm/min_similarity
Translation memory's find_similar() ranks fuzzy matches by PHP's similar_text() percentage. By default it returns the top-N suggestions regardless of how low the similarity is. Use this filter to suppress suggestions below a confidence threshold so the editor UI only shows useful candidates.
// Only surface fuzzy suggestions that are at least 60% similar.
add_filter( 'perflocale/tm/min_similarity', fn() => 60.0 );
// Auto-apply territory: only use TM hits when they're 90%+ similar.
add_filter( 'perflocale/tm/min_similarity', fn() => 90.0 );Common values:
0(default) — return everything that survived the SQL pre-filter (3-significant-words AND, length window).60— "decent match" cut-off recommended for editor UI.75— "high-confidence" threshold, suitable for auto-fill suggestions.90+— near-exact only, suitable for unattended pipelines.
Parameters: float $threshold - Default 0.0 (no threshold).
File: src/Translation/TranslationMemory.php
perflocale/mt/pre_translate_text
Filter the source text right before it is sent to the MT provider. Lets you mask placeholders, strip shortcodes, or pre-substitute branded terms beyond what the glossary covers.
add_filter( 'perflocale/mt/pre_translate_text', function ( string $text, string $source_lang, string $target_lang, string $provider_id ): string {
// Strip our internal {ATTR:foo} placeholders so the MT provider doesn't see them.
return preg_replace( '/\{ATTR:[a-z_]+\}/', '', $text );
}, 10, 4 );Parameters: string $text, string $source_lang, string $target_lang, string $provider_id.
File: src/MachineTranslation/TranslationService.php
perflocale/mt/post_translate_text
Filter the translated text after the MT provider returns it (and after glossary re-injection / kses sanitisation). The fifth argument is the provider id so a single callback can branch by provider.
add_filter( 'perflocale/mt/post_translate_text', function ( string $translated, string $source, string $source_lang, string $target_lang, string $provider_id ): string {
// DeepL sometimes leaves a stray space before German punctuation; tidy it.
if ( $provider_id === 'deepl' && $target_lang === 'de' ) {
$translated = preg_replace( '/\s+([,.;:!?])/', '$1', $translated );
}
return $translated;
}, 10, 5 );Parameters: string $translated, string $source, string $source_lang, string $target_lang, string $provider_id.
File: src/MachineTranslation/TranslationService.php
perflocale/mt/agency_async_response
Handle responses from the External Agency provider when the agency does not return a synchronous translation in the immediate POST response. PerfLocale does not ship a built-in async callback receiver; this filter is the supported extension point for addons that want to add one. The default return is null, which causes the provider to throw RuntimeException rather than silently fall back to source text (the pre-1.x behaviour, which corrupted translation posts).
Return a non-empty string to use as the translation. Return null (or empty string) to fall through to the throw — appropriate when the addon hasn't received the agency callback yet.
// Sketch of an out-of-tree async receiver: an addon registers its own
// REST route (e.g. POST /mycorp/v1/agency-callback) with HMAC verification,
// stashes incoming translations into an option/transient keyed by
// request_id, and matches them back here when the next translate() runs.
add_filter( 'perflocale/mt/agency_async_response', function (
$translation,
array $response,
string $request_id,
array $payload,
array $context
) {
$cached = get_option( 'mycorp_agency_cb_' . $request_id );
if ( is_array( $cached ) && isset( $cached['translation'] ) ) {
delete_option( 'mycorp_agency_cb_' . $request_id );
return (string) $cached['translation'];
}
// No callback yet — let the core throw so the job fails loudly.
return null;
}, 10, 5 );When this fires: the agency returned a 2xx response but the decoded body has no translation string key. The previous behaviour was to silently return the source text and bill monthly characters; the current behaviour throws unless this filter supplies a string. See Machine Translation → External Translation Agency.
Parameters:
string|null $translation— Defaultnull(causes the provider to throw).array $response— Decoded agency response body.string $request_id— UUIDv4 generated for this outbound request. Use it to correlate against a previously-received agency callback.array $payload— Outbound payload sent to the agency:text,source_lang,target_lang,request_id.array $context— Provider context:source_lang,target_lang,provider_id(alwaysexternal_agency).
Args: 5
File: src/MachineTranslation/Provider/ExternalAgencyProvider.php
perflocale/mt/use_translation_memory
Per-request override of the Translation Memory toggle for block-level MT calls. Default is whatever the saved mt_use_translation_memory setting says. Return false to skip the TM lookup and force a fresh provider call.
// Skip TM for very short snippets so they always use fresh provider quality.
add_filter( 'perflocale/mt/use_translation_memory', function ( bool $use_tm, string $text, string $source_lang, string $target_lang ): bool {
return mb_strlen( $text ) >= 16 && $use_tm;
}, 10, 4 );Parameters: bool $use_tm, string $text, string $source_lang, string $target_lang.
File: src/Api/BlockTranslateController.php
WordPress 6.9+ / 7.0+ Integrations (feature-detected — inert on earlier WordPress versions)
Hooks that wire PerfLocale into recent WordPress APIs: the Abilities API and its JS-side shim (WordPress 6.9+), and the AI Client, the Connectors API, and Block Hooks (WordPress 7.0+). Each surface is feature-detected — the Abilities filters (perflocale/abilities/js_screens, perflocale/abilities/js_payload) light up once wp_register_ability() exists (WordPress 6.9+); the AI Client surfaces require WordPress 7.0+. On a site without the relevant API these filters still fire but their underlying code paths stay inert.
perflocale/mt/wp_ai_client_resolver
Override the callable that WpAiClientProvider invokes. Useful for tests, custom in-house AI gateways, or as a forward-compatibility shim if WordPress core renames the underlying function.
add_filter( 'perflocale/mt/wp_ai_client_resolver', function () {
return function ( string $prompt, array $args ): string {
// Route to your own gateway and return raw model text.
return my_gateway_complete( $prompt, $args );
};
} );Parameters: null|callable $resolver - Callable accepting (string $prompt, array $args). Return null to fall through to auto-detection.
File: src/MachineTranslation/Provider/WpAiClientProvider.php
perflocale/mt/wp_ai_client_prompt
Customise the structured prompt sent to the AI Client per translation request. Keep the rules about placeholder preservation (the integrity gate enforces them) and the "output only the translation" instruction.
add_filter( 'perflocale/mt/wp_ai_client_prompt', function ( string $prompt, string $text, string $source, string $target ): string {
// Inject brand-voice guidance.
return $prompt . "\n\nAlways translate as if writing for a friendly, conversational SaaS audience.";
}, 10, 4 );Parameters: string $prompt, string $text, string $source_lang, string $target_lang.
File: src/MachineTranslation/Provider/WpAiClientProvider.php
perflocale/mt/wp_ai_client_args
Filter the argument array passed to the AI Client call (capability, temperature, timeout, custom provider/model selectors).
add_filter( 'perflocale/mt/wp_ai_client_args', function ( array $args, bool $fast_fail ): array {
$args['provider'] = 'anthropic';
$args['model'] = 'claude-opus-4-7';
return $args;
}, 10, 2 );Parameters: array $args, bool $fast_fail.
File: src/MachineTranslation/Provider/WpAiClientProvider.php
perflocale/mt/wp_ai_client_capability
Filter the capability tag the AI Client should match. Defaults to 'text-generation'. Set to 'translation' or another capability tag if your AI gateway routes calls by capability.
add_filter( 'perflocale/mt/wp_ai_client_capability', fn() => 'translation' );Parameters: string $capability.
File: src/MachineTranslation/Provider/WpAiClientProvider.php
perflocale/mt/quality_score_supported
Force-enable or force-disable the MT quality-scoring job's gate. Default auto-detects the AI Client and any custom resolver. Use this to enable scoring on a site running a non-WP-AI-Client AI gateway, or to disable it even when the AI Client is present.
// Enable scoring against a custom AI resolver.
add_filter( 'perflocale/mt/quality_score_supported', '__return_true' );Parameters: bool $supported - Auto-detected support.
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/mt/quality_sample_size
Per-run sample size for the scoring cron. Default 50. Larger = faster coverage of the whole catalogue, smaller = lighter provider load and lower cost.
add_filter( 'perflocale/mt/quality_sample_size', fn() => 20 );Parameters: int $sample_size.
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/mt/quality_score_tables
Restrict or extend the tables that the scoring sweep visits. Unknown table keys are skipped silently. Useful for cost-control (score strings only, not posts) or for adding a custom store mid-migration.
// Only score string translations, never post translations.
add_filter( 'perflocale/mt/quality_score_tables', fn() => [ 'string_translations' ] );Parameters: string[] $tables, string $target (the target_table arg).
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/mt/quality_score_skip_row
Skip a specific row from scoring before any AI call is made. Useful for excluding legal disclaimers, low-traffic languages, RTL languages where you don't yet have a tuned model, or strings below a length threshold.
add_filter( 'perflocale/mt/quality_score_skip_row', function ( bool $skip, object $row, string $table ): bool {
if ( isset( $row->translation ) && mb_strlen( (string) $row->translation ) < 40 ) {
return true; // too short to be worth scoring
}
return $skip;
}, 10, 3 );Parameters: bool $skip, object $row, string $table.
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/mt/quality_score_prompt
Tune the 1-5 scoring prompt for your domain. Legal-content sites can weight regulatory accuracy; marketing sites can weight brand voice. Keep the "output only a single digit" instruction or the response parser will mark the row as skipped.
add_filter( 'perflocale/mt/quality_score_prompt', function ( string $prompt, object $row, string $source, string $translation ): string {
return "Rate this translation 1-5 with extra weight on preservation of regulated medical terminology.\n" .
"Source:\n$source\n\nTranslation:\n$translation\n\nOutput only 1, 2, 3, 4, or 5.";
}, 10, 4 );Parameters: string $prompt, object $row, string $source_text, string $translated_text, string $source_slug, string $target_slug.
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/switcher/auto_insert_anchor
Retarget the Block Hooks auto-insertion of the language-switcher block to a different anchor. The default is core/site-title:after. Return a [ block_type, position ] pair, or false to keep the default.
// Put the switcher at the end of the site navigation instead of after the title.
add_filter( 'perflocale/switcher/auto_insert_anchor', fn() => [ 'core/navigation', 'last_child' ] );Parameters: array|false $override, string $anchor_block_type, string $relative_position.
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/switcher/auto_insert_attrs
Override the per-context default attributes the switcher carries when auto-inserted by Block Hooks. Defaults to dropdown display + compact 16-px flag for header context. Return an empty array to fall back to the global Settings → Language Switcher values.
add_filter( 'perflocale/switcher/auto_insert_attrs', function ( array $attrs, string $anchor, string $position ): array {
$attrs['style'] = 'flags_only';
$attrs['flagSize'] = 14;
return $attrs;
}, 10, 3 );Parameters: array $attrs, string $anchor_block_type, string $relative_position, array|null $parsed_anchor_block.
File: src/Frontend/LanguageSwitcherBlock.php
perflocale/abilities/js_screens
Extend the list of admin screens where the JS Abilities shim is enqueued. By default the shim ships on post.php, post-new.php, and site-editor.php — sites running a custom block-editor surface can opt that hook in here.
add_filter( 'perflocale/abilities/js_screens', function ( array $hooks ): array {
$hooks[] = 'toplevel_page_my-custom-editor';
return $hooks;
} );Parameters: string[] $screens.
File: src/Admin/Assets.php
perflocale/abilities/js_payload
Filter the payload localised into the JS Abilities shim. Use this to remove specific abilities from JS-side registration (e.g. keep translate-post server-only) or to inject custom ability descriptors. The shape is [ 'abilities' => [ { name, label, ... }, ... ] ].
add_filter( 'perflocale/abilities/js_payload', function ( array $payload ): array {
$payload['abilities'] = array_filter(
$payload['abilities'] ?? [],
fn( array $a ): bool => $a['name'] !== 'perflocale/translate-post'
);
return $payload;
} );Parameters: array $payload.
File: src/Admin/Assets.php
perflocale/connectors/resolver
Override the callable used to look up keys in the WordPress Connectors API. Useful for tests, custom in-house key vaults, or as a forward-compatibility shim if core renames the underlying function.
add_filter( 'perflocale/connectors/resolver', function () {
return function ( string $slug ): ?string {
// Pull from your own secrets store.
return MyVault::get_key( $slug );
};
} );Parameters: null|callable $resolver.
File: src/Settings.php
perflocale/connectors/slug_map
Extend the setting-key → connector-slug map to route additional API keys through the Connectors API (defaults cover the 4 MT providers only). Add geo provider keys, currency-rate keys, or any other plugin secret you'd like the Connectors API to broker.
add_filter( 'perflocale/connectors/slug_map', function ( array $map ): array {
$map['geo_ipinfo_token'] = 'ipinfo';
$map['geo_ipstack_key'] = 'ipstack';
$map['wc_oxr_api_key'] = 'openexchangerates';
return $map;
} );Parameters: array<string, string> $map - perflocale_setting_key => connector_slug.
File: src/Settings.php
Query Filtering
perflocale/query/include_all_languages
Override language filtering to include all languages in a query.
add_filter( 'perflocale/query/include_all_languages', function ( bool $include, WP_Query $query ): bool {
if ( $query->get( 'post_type' ) === 'faq' ) {
return true; // Show FAQs in all languages.
}
return $include;
}, 10, 2 );Parameters:
bool $include- Whether to include all languages (defaultfalse).WP_Query $query- The query object.
File: src/Translation/PostQueryFilter.php
Addons
perflocale/addons/registered
Filter the list of registered addons.
Parameters: array $addons - Array of addon definitions.
File: src/Addon/AddonRegistry.php
Dev-mode guard: a return whose shape is not array<string, AddonInterface> triggers _doing_it_wrong() and the registry falls back to the unfiltered addon list.
perflocale/addon/delete_data_on_uninstall
Per-addon override of the global Delete data on uninstall setting. Lets a single addon opt out of (or into) data deletion regardless of the plugin-wide preference. Receives the planned PurgePlan so callers can branch on which tables / option keys would be removed.
add_filter( 'perflocale/addon/delete_data_on_uninstall', function ( bool $delete, string $addon_id, $plan ): bool {
// Always preserve the WooCommerce addon's data even if the user toggled global deletion on.
if ( $addon_id === 'woocommerce' ) {
return false;
}
return $delete;
}, 10, 3 );Parameters: bool $delete, string $addon_id, PerfLocale\Addon\PurgePlan $plan.
File: src/Addon/AddonUninstaller.php
perflocale/addon/is_compatible
Filter whether an addon is compatible with the current environment.
Parameters:
bool $compatible- Compatibility result.string $addon_id- Addon identifier.
File: src/Addon/AddonRegistry.php
perflocale/addons/quarantine_threshold
How many consecutive boot failures an addon is allowed before the registry auto-quarantines it (skipping its boot() on subsequent requests). The default of 3 tolerates transient issues like a missing dependency during an upgrade window or a one-off OOM; raise it for flaky network-dependent addons, lower it to fail-fast in development.
// Raise tolerance for a known-flaky addon to 10 consecutive failures.
add_filter( 'perflocale/addons/quarantine_threshold', fn() => 10 );
// Quarantine on the very first failure (strict).
add_filter( 'perflocale/addons/quarantine_threshold', fn() => 1 );
// Disable quarantine entirely (every failure is retried on next request).
add_filter( 'perflocale/addons/quarantine_threshold', fn() => 0 );The result is memoised per request so the filter only fires once even when many addons boot. Returning 0 (or a negative number) disables quarantine entirely; 1 quarantines on the first failure. Operators see quarantined addons in the Addons admin page with the last error message inline, and can reset via wp perflocale addon reset-quarantine <id>.
Parameters: int $threshold — Default 3. Min effective value 1; values ≤ 0 disable quarantine.
File: src/Addon/AddonRegistry.php
perflocale/addon/manifest/check_soft_prefix_namespacing
Suppress the developer-mode _doing_it_wrong nudge that fires when an addon's soft-prefix uninstall-manifest entries (transient prefixes, cron hooks, post/user/term/comment meta keys) don't include the per-addon sub-namespace (perflocale_{addon_id}_, plus _perflocale_{addon_id}_ for meta keys).
The notice is purely a DX hint — soft-prefix per-addon isolation is not enforced (see Hard vs soft prefixes), and the global perflocale_ prefix is the only namespace requirement. The notice is also WP_DEBUG-gated by core, so production users never see it. Filter it off for addons that intentionally coordinate across the addon namespace (e.g. a manager addon cleaning up after a family of related sibling addons).
// Suppress for one specific coordinating addon.
add_filter( 'perflocale/addon/manifest/check_soft_prefix_namespacing', function ( bool $check, string $addon_id ): bool {
return 'mycorp_coordinator' === $addon_id ? false : $check;
}, 10, 2 );
// Suppress globally during a noisy migration.
add_filter( 'perflocale/addon/manifest/check_soft_prefix_namespacing', '__return_false' );Parameters:
bool $check— Defaulttrue. Returnfalseto skip the check for this addon.string $addon_id— Identifier of the addon whose manifest is being written.
Args: 2
File: src/Addon/AddonManifestWriter.php
WooCommerce
perflocale/woocommerce/exchange_rate_providers
Register or modify exchange rate API providers.
Parameters: array $providers - Provider definitions (see Exchange Rates docs).
File: src/WooCommerce/ExchangeRateSync.php
perflocale/woocommerce/exchange_rates_fetched
Filter rates after they are fetched from the API but before saving. Use to add a markup or override specific rates.
Parameters:
array $rates- Currency code => exchange rate.string $base_currency- WooCommerce base currency code.string $provider_id- Selected provider ID.
File: src/WooCommerce/ExchangeRateSync.php
perflocale/woocommerce/synced_product_fields
Control which product meta keys are synced across language variants.
Parameters:
array $fields- Meta key list.int $product_id- Product being synced.
File: src/WooCommerce/InventorySync.php
perflocale/woocommerce/skip_inventory_sync
Skip inventory sync for a specific product.
Parameters:
bool $skip- Default false.int $product_id- Product being synced.
File: src/WooCommerce/InventorySync.php
perflocale/woocommerce/translate_string
Override string translation for WooCommerce gateway/shipping titles.
Parameters:
string|null $translated- Translated string or null (no override).string $original- Original string.string $slug- Current language slug.string $context- Translation context (gateway ID or empty).
Args: 4
File: src/WooCommerce/WcStringTranslation.php
perflocale/woocommerce/translatable_email_ids
Filter the WooCommerce email IDs whose subjects, headings, and additional content are translatable via the String Translation system.
add_filter( 'perflocale/woocommerce/translatable_email_ids', function ( array $ids ): array {
$ids[] = 'customer_stock_notification'; // Add a custom WC email type.
return $ids;
} );Parameters: array $ids - Array of WooCommerce email ID strings.
Default: new_order, cancelled_order, failed_order, customer_on_hold_order, customer_processing_order, customer_completed_order, customer_refunded_order, customer_invoice, customer_note
Args: 1
File: src/WooCommerce/EmailTranslation.php
GeoIP Redirect
perflocale/redirect/priority_order
Per-request override of the saved redirect priority order. Receives the cleaned, sanitised order array — useful for forcing geo first for staff cookie holders, or falling back to browser while a third-party geo provider is rate-limited.
add_filter( 'perflocale/redirect/priority_order', function ( array $order ): array {
if ( isset( $_COOKIE['acme_staff'] ) ) {
// Trust GeoIP first for the team, then fall back to browser language.
return [ 'geo', 'browser', 'edge_hint' ];
}
return $order;
} );Parameters: array $order - Sanitised priority array. Known sources: geo, browser, edge_hint (any other slug is dropped by the sanitiser). Default order: geo, browser, edge_hint.
File: src/Settings.php
perflocale/geo/providers
Filter available GeoIP providers. Add custom providers or modify built-in ones.
add_filter( 'perflocale/geo/providers', function ( array $providers ): array {
$providers['my_provider'] = [
'name' => 'My GeoIP Provider',
'needs_key' => true,
'key_setting' => 'geo_my_provider_key',
'fetch_callback' => function ( string $ip, \PerfLocale\Settings $settings ): string {
// Return two-letter country code or ''.
return 'US';
},
];
return $providers;
} );Parameters: array $providers - Associative array of provider definitions.
File: src/Router/GeoRedirect.php
perflocale/geo/country_code
Filter the detected country code after a GeoIP lookup.
Parameters: string $country_code, string $ip
File: src/Router/GeoRedirect.php
perflocale/geo/redirect_language
Override the language slug selected for redirect.
Parameters: string $language_slug, string $country_code, string $ip
File: src/Router/GeoRedirect.php
perflocale/geo/country_map
Filter the country-to-language mapping array.
Parameters: array $map - Associative array of language slug => comma-separated country codes.
File: src/Router/GeoRedirect.php
perflocale/geo/should_redirect
Final short-circuit before the outbound IP-lookup HTTP request is made. Return false to skip GeoIP redirection entirely for the current request - no provider call is issued. Complements the existing perflocale/privacy/consent_given gate.
// Skip GeoIP redirects for logged-in users, admin URLs, and affiliate campaigns.
add_filter( 'perflocale/geo/should_redirect', function ( bool $should ): bool {
if ( is_user_logged_in() ) {
return false; // Respect the visitor’s saved preference.
}
if ( ! empty( $_GET['utm_campaign'] ) ) {
return false; // Don’t second-guess campaign traffic.
}
$path = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH ) ?: '/';
if ( str_starts_with( $path, '/api/' ) || str_starts_with( $path, '/webhook/' ) ) {
return false;
}
return $should;
} );Parameters: bool $should_redirect - default true.
File: src/Router/GeoRedirect.php
perflocale/fallback/redirect_status
HTTP status code used when a language fallback redirect fires. Default 302 (temporary). Return 301 for permanent SEO consolidation. Allowed values: {301, 302, 307, 308}; anything else silently reverts to 302.
// Permanent redirects for en_US → en_GB specifically.
add_filter( 'perflocale/fallback/redirect_status', function ( int $status, string $from, string $to ): int {
return ( $from === 'en-us' && $to === 'en-gb' ) ? 301 : $status;
}, 10, 3 );Parameters:
int $status- default 302.string $from_slug- current (missing) language slug.string $to_slug- resolved fallback language slug.int $from_post_id- source post being rendered.int $to_post_id- target post ID (0when redirecting to the language homepage).
File: src/Translation/PostQueryFilter.php
Privacy
perflocale/privacy/consent_given
Gate for PerfLocale’s perflocale_lang cookie and its automatic GeoIP + browser-language redirects. Return false to suppress the cookie and both redirects until the visitor has granted consent. Default true - they fire immediately, matching the pre-consent-framework behaviour. (To drop the cookie unconditionally without a consent plugin, use the Cookieless-mode setting instead.)
// Hook any consent-management plugin (Cookiebot, Complianz, Iubenda,
// OneTrust...) that exposes a "consent given for marketing/functional
// cookies" check.
add_filter( 'perflocale/privacy/consent_given', function (): bool {
if ( ! function_exists( 'complianz_has_consent' ) ) {
return true; // Consent plugin not active — behave as before.
}
return (bool) complianz_has_consent( 'functional' );
} );Parameters: bool $granted - default true.
File: src/Router/LanguageRouter.php, src/Router/GeoRedirect.php
REST API
perflocale/api/config
Filter the JSON payload returned by GET /perflocale/v1/config before it is cached and served to edge runtimes (Cloudflare Worker, Vercel Edge, Netlify Edge, …). Use it to carry feature flags, per-language A/B variants, fallback chains, or any routing metadata the edge needs without an extra origin round-trip.
// Expose feature flags + fallback chain to the edge.
add_filter( 'perflocale/api/config', function ( array $payload ): array {
$payload['feature_flags'] = [
'new_checkout' => get_option( 'acme_new_checkout_enabled', false ),
'promo_banner' => get_option( 'acme_promo_banner_active', false ),
];
$payload['fallback_chain'] = [
'en-gb' => [ 'en-us', 'en' ],
'de-at' => [ 'de', 'en' ],
];
return $payload;
} );The result is stored in the 3-layer cache and contributes to the ETag. Keep additions deterministic (no timestamps, no request-specific data). If the custom fields change at runtime, call \PerfLocale\Api\ConfigController::invalidate() or update any option/setting that already triggers perflocale/settings/updated.
Parameters: array $payload - The full config payload (version, url_mode, default_slug, hide_default_prefix, excluded_paths, detection_order, edge_hint_header, edge_hint_cookie, languages).
File: src/Api/ConfigController.php
perflocale/api/languages_public
Whether the public read endpoints GET /perflocale/v1/languages and GET /perflocale/v1/languages/{slug} may be accessed anonymously. Default true - the payload is a strict subset of what is already rendered to visitors (switcher, hreflang tags, URL structure), so there is nothing private to protect. Return false to require the read capability (any logged-in user) instead. Write endpoints are unaffected - they always require perflocale_manage_languages.
// Block anonymous reads of /languages.
add_filter( 'perflocale/api/languages_public', '__return_false' );Parameters:
bool $public- defaulttrue.WP_REST_Request $request- current request.
Args: 2
File: src/Api/LanguagesController.php
perflocale/edge_worker/config_permission_callback
Gate for the public read endpoint GET /wp-json/perflocale/v1/config (consumed by the Cloudflare Worker / Vercel Edge / Netlify Edge integrations). Default true — public-read, because the payload is a strict subset of what is already rendered to visitors (URL mode, default slug, active languages, edge-hint header / cookie names). Return false or a WP_Error to require authentication; return true to keep public-read. Symmetric with perflocale/api/languages_public for the /languages endpoint — write endpoints are unaffected and always require perflocale_manage_languages.
// Require admin auth to fetch the edge config.
add_filter( 'perflocale/edge_worker/config_permission_callback', static fn() => current_user_can( 'manage_options' ) );
// Allow internal monitoring (header-presented bearer) but block everyone else.
add_filter( 'perflocale/edge_worker/config_permission_callback', function ( $allowed, \WP_REST_Request $request ) {
$bearer = $request->get_header( 'authorization' );
if ( $bearer && hash_equals( 'Bearer ' . MY_EDGE_TOKEN, $bearer ) ) {
return true;
}
return new \WP_Error( 'rest_forbidden', 'Edge config requires auth on this host.', [ 'status' => 401 ] );
}, 10, 2 );If the filter returns anything other than bool / WP_Error, PerfLocale emits a _doing_it_wrong() notice (dev-mode) and falls back to the default public-read behaviour rather than crashing the endpoint.
Parameters:
bool|WP_Error $allowed— defaulttrue.WP_REST_Request $request— current request.
Args: 2
File: src/Api/ConfigController.php
Admin
perflocale/admin/filter_terms_checklist
Whether the post-edit Categories / Tags checklist should be filtered to the current language. Default true when 2+ languages are active. Return false to show every term across every language in the metabox (e.g. for a custom integration that re-scopes terms in JS).
add_filter( 'perflocale/admin/filter_terms_checklist', function ( bool $enabled, int $post_id, array $args ): bool {
// Don't filter for editors managing the term taxonomy itself.
return current_user_can( 'manage_categories' ) ? false : $enabled;
}, 10, 3 );Parameters: bool $enabled, int $post_id, array $args.
File: src/Admin/MetaBox.php
perflocale/admin/post_list_columns
Filter the columns added to the post list table.
Parameters:
array $columns- Column definitions.string $post_type- Current post type.
File: src/Admin/PostListColumns.php
perflocale/predefined_languages
Filter the bundled list of predefined languages shown in the PerfLocale → Languages → Add New quick-select. The plugin ships with 190+ entries covering every major locale plus regional variants (en-US, en-GB, fr-CA, ar-EG, etc.); use this filter to add custom languages (constructed languages, internal locale variants, niche dialects), prune the bundled set, or replace it entirely with a curated short-list for site editors.
Each entry must be an associative array with these keys:
slug- URL-safe slug. Maximum 10 characters, must be unique across all entries.locale- WordPress locale (e.g.en_US,fr_FR). Maximum 20 characters, must be unique.name- English display name shown in the admin picker.native_name- Native-script display name shown alongside the English name.flag- ISO 3166-1 alpha-2 country code (lowercase, e.g.us,fr); used to render the flag emoji. Empty string is allowed for languages without an obvious flag.text_direction-'ltr'or'rtl'.date_format- PHPdate()format string. Often locale-specific (e.g.'j F Y'for European day-month-year).time_format- PHPdate()format string (e.g.'g:i a','H:i').
Add a custom language:
add_filter( 'perflocale/predefined_languages', function ( array $list ): array {
$list[] = [
'slug' => 'tlh',
'locale' => 'tlh_KL',
'name' => 'Klingon',
'native_name' => 'tlhIngan Hol',
'flag' => '',
'text_direction' => 'ltr',
'date_format' => 'F j, Y',
'time_format' => 'g:i a',
];
return $list;
} );Restrict the picker to a curated short-list (e.g. for editors who should only ever pick from your supported locales):
add_filter( 'perflocale/predefined_languages', function ( array $list ): array {
$allowed = [ 'en', 'es', 'fr', 'de', 'ja' ];
return array_values( array_filter(
$list,
fn( array $entry ) => in_array( $entry['slug'] ?? '', $allowed, true )
) );
} );Override a bundled entry (e.g. swap the default date_format for German to ISO 8601):
add_filter( 'perflocale/predefined_languages', function ( array $list ): array {
foreach ( $list as &$entry ) {
if ( ( $entry['slug'] ?? '' ) === 'de' ) {
$entry['date_format'] = 'Y-m-d';
}
}
return $list;
} );Notes: This filter only affects the admin quick-select shown when adding a new language. Once a language is saved into wp_perflocale_languages, the filter no longer touches it — existing rows are managed via the regular admin Edit screen or the REST API. Removing an entry from the filter does not remove already-added languages.
Parameters: array<int, array<string, string>> $predefined - Bundled languages as loaded from data/languages.php (190+ entries in 1.0.0).
File: src/Admin/Pages/LanguagesPage.php
Roles & Permissions
perflocale/roles/editor_caps
Filter the capabilities granted to the Editor role on plugin activation. Return an empty array to prevent Editors from receiving any PerfLocale capabilities. Return a subset to restrict them to specific ones. The Administrator role is not affected.
Parameters: array<string, bool> $caps - Map of capability => grant. Default: perflocale_translate, perflocale_manage_translations, perflocale_manage_glossary, perflocale_use_mt.
File: src/Admin/TranslatorRole.php
// Remove ALL PerfLocale capabilities from the Editor role.
add_filter( 'perflocale/roles/editor_caps', '__return_empty_array' );
// Restrict Editors to translation-only access (no management capabilities).
add_filter( 'perflocale/roles/editor_caps', function ( array $caps ): array {
return [
'perflocale_translate' => true,
'perflocale_use_mt' => true,
];
} );Note: Capability grants are stored in the database and only written once per version. To re-apply the filter to an existing install, reset the version flag: delete_option( 'perflocale_caps_version' ); then reload any admin page.
perflocale/roles/cap_roles
Filter which WordPress roles have PerfLocale capabilities removed on plugin deactivation or uninstall. Fires in three places: TranslatorRole::remove_roles(), and both the full-wipe and preserve-data branches of uninstall.php.
Parameters: string[] $roles - Array of role slugs. Default: ['administrator', 'editor'].
File: src/Admin/TranslatorRole.php, uninstall.php
// Do not strip caps from the Editor role on deactivation/uninstall.
add_filter( 'perflocale/roles/cap_roles', function ( array $roles ): array {
return array_diff( $roles, [ 'editor' ] );
} );
// Also clean up a custom role that was granted caps programmatically.
add_filter( 'perflocale/roles/cap_roles', function ( array $roles ): array {
$roles[] = 'shop_manager';
return $roles;
} );Performance & Caching
perflocale/cache/eager_map_row_cap
Filter the maximum number of translation-link rows held in the autoloaded eager-link-map option (perflocale_eager_links_post / perflocale_eager_links_term). When the per-type link count is at or below this cap, every prime_translations() call is served from alloptions with zero queries. Past the cap, the map is replaced with a 'too_large' sentinel and the plugin falls back to the per-key cascade in CacheManager — on those sites a persistent object cache (Redis / Memcached) is what keeps the hot path fast.
Default 2000. Lower on memory-constrained hosts where a large alloptions blob hurts more than the extra DB round-trips. Raise on memory-rich hosts that prefer a single autoloaded option over hitting the cache cascade at scale. Receives the ObjectType enum as the second arg so post and term maps can be tuned independently.
add_filter( 'perflocale/cache/eager_map_row_cap', function ( int $cap, \PerfLocale\Enum\ObjectType $type ): int {
// Roomy host: raise the post cap to 5000, keep the term cap at default.
return $type === \PerfLocale\Enum\ObjectType::Post ? 5000 : $cap;
}, 10, 2 );Parameters: int $cap (default 2000), \PerfLocale\Enum\ObjectType $type.
File: src/Database/Repository/TranslationGroupRepository.php
perflocale/cache/eager_map_byte_cap
Defensive byte-size gate for the same eager-link-map option. After the row cap passes, the serialised array is measured one more time — if it exceeds this byte cap the 'too_large' sentinel is written instead. Catches the edge case of an under-the-row-cap map whose individual rows carry unusually large fields (long language slugs, custom addon columns, etc.).
Default 768000 bytes (750 KB). Tune in tandem with the row cap on hosts with non-default alloptions sizing.
add_filter( 'perflocale/cache/eager_map_byte_cap', static fn (): int => 1024 * 1024 ); // 1 MB ceilingParameters: int $cap (default 768000), \PerfLocale\Enum\ObjectType $type.
File: src/Database/Repository/TranslationGroupRepository.php
Background Jobs
Filters that tune the background-processing system. See the Background Jobs doc for the full feature reference.
perflocale/jobs/threshold/<type>
Per-type override of the Auto-mode threshold. When args_size() for a dispatch is at or above this threshold, the job goes async; below, it runs inline. Defaults per job type: 1000 for string_scan, 1000 for data_import / glossary_import, 500 for wpml_migration / polylang_migration, 200 for translatepress_migration, 5000 for data_export.
// Force string scans to ALWAYS run async, regardless of file count.
add_filter( 'perflocale/jobs/threshold/string_scan', static fn(): int => 0 );
// Dynamic threshold for data imports: queue big files, inline small ones.
add_filter( 'perflocale/jobs/threshold/data_import', static function ( int $base, array $args ): int {
$file = $args['file_path'] ?? '';
return file_exists( $file ) && filesize( $file ) > 5 * MB_IN_BYTES ? 0 : 999999;
}, 10, 2 );Parameters: int $base (resolved from settings or default), array $args (the dispatch args).
File: src/Background/AbstractJob.php
perflocale/jobs/max_attempts
Maximum retry count before a failed job stops being rescheduled. Default 5 (initial attempt + 4 retries).
add_filter( 'perflocale/jobs/max_attempts', static fn(): int => 1 ); // one-shot, never retryParameters: int $max.
File: src/Background/WorkerRegistry.php
perflocale/jobs/retry_delay
Seconds to wait before the next retry attempt. Default: exponential backoff capped at 1 hour — min(3600, 60 * 2^(attempts-1)).
add_filter( 'perflocale/jobs/retry_delay', static function ( int $delay, int $attempts ): int {
return 60 * max( 1, $attempts ); // linear backoff
}, 10, 2 );Parameters: int $delay, int $attempts.
File: src/Background/WorkerRegistry.php
perflocale/jobs/max_concurrent/<type>
Per-type concurrency cap. Default 1 (one worker per type at a time). Set > 1 to allow parallel workers; PHP_INT_MAX disables the cap entirely.
// Allow up to 4 parallel string scans.
add_filter( 'perflocale/jobs/max_concurrent/string_scan', static fn(): int => 4 );Parameters: int $max.
File: src/Background/WorkerRegistry.php
perflocale/jobs/max_args_bytes
Maximum JSON-encoded size of the args payload accepted by Dispatcher::enqueue(). Default 100 KB. Hardening against options-table bloat.
Parameters: int $bytes (clamped to a minimum of 1024).
File: src/Background/Dispatcher.php
perflocale/jobs/max_args_depth
Maximum array-nesting depth of the args payload accepted by Dispatcher::enqueue(). Default 20. A payload can fit under the byte cap (max_args_bytes) yet still be pathologically deep — deeply-nested arrays trip PHP’s serializer (which WP uses to write option_value) and can blow xdebug.max_nesting_level. Args deeper than this are rejected before serialisation. Floor-clamped to 4, so any legitimate job-args shape is safe.
// A custom job that ships a genuinely nested config tree.
add_filter( 'perflocale/jobs/max_args_depth', static fn(): int => 40 );Parameters: int $depth (default 20, clamped to a minimum of 4).
File: src/Background/Dispatcher.php
perflocale/jobs/active_index_max
Cap on the active-jobs index size (the rows visible on PerfLocale → Jobs). Default 50. Each row is ~200 bytes, so a cap of 250 costs ~50 KB in a non-autoloaded option. Floor-clamped to 10.
// Sites with very high dispatch throughput can show more history.
add_filter( 'perflocale/jobs/active_index_max', static fn(): int => 250 );Parameters: int $max (default 50, clamped to 10 minimum).
File: src/Background/JobState.php
perflocale/import/max_file_bytes
Maximum size of the uploaded JSON import envelope. Default 52428800 (50 MB). Floor-clamped to 1 MB.
add_filter( 'perflocale/import/max_file_bytes', static fn(): int => 200 * 1024 * 1024 ); // 200 MBParameters: int $bytes.
File: src/Admin/AdminController.php
perflocale/export/batch_size
Rows fetched per LIMIT clause during the streaming JSON export. Default 1000. Bottleneck is usually wp_json_encode, not the SELECT. Clamped to 50–10000. Per-table override via the second arg.
add_filter( 'perflocale/export/batch_size', static function ( int $size, string $table ): int {
return $table === 'strings' ? 5000 : 1000;
}, 10, 2 );Parameters: int $size, string $table.
File: src/Admin/DataExporter.php
perflocale/migration/translatepress/batch_size
Posts processed per transaction during the TranslatePress migration. Default 50. Clamped to 5–500.
add_filter( 'perflocale/migration/translatepress/batch_size', static fn(): int => 200 );Parameters: int $size.
File: src/Migration/TranslatePressImporter.php
perflocale/migration/wpml/batch_size
Translation groups (trid values) fetched per SELECT during the WPML migration. The importer first pulls every distinct trid for posts then for terms (cheap, one BIGINT per row), chunks them by this filter, and only fetches the full (trid, element_id, language_code) rows one batch at a time. Lowering the value reduces peak memory on very large sites at the cost of more SQL roundtrips. Default 100. Clamped to 10–1000.
add_filter( 'perflocale/migration/wpml/batch_size', static fn(): int => 250 );Parameters: int $size.
File: src/Migration/WpmlImporter.php
perflocale/migration/polylang/batch_size
Number of post_translations / term_translations taxonomy terms fetched per SELECT during the Polylang migration. The importer first lists every term_id (cheap), chunks them by this filter, and only then fetches the description payload (a serialized PHP array that maybe_unserialize expands into N slots in memory per row). Lowering the value reduces peak memory on very large sites at the cost of more SQL roundtrips. Default 100. Clamped to 10–1000.
add_filter( 'perflocale/migration/polylang/batch_size', static fn(): int => 250 );Parameters: int $size.
File: src/Migration/PolylangImporter.php
perflocale/migration/wpml_string_batch_size
Rows fetched per keyset-paginated batch when importing string translations from WPML's icl_string_translations table. The importer streams via WHERE s.id > $last_id ORDER BY s.id ASC LIMIT N so peak memory stays bounded at one batch regardless of total row count — on a 50k-string WPML export the previous one-shot SELECT consumed ~25 MB of PHP arrays before any work began. Lowering the value trades fewer SQL roundtrips for smaller memory peaks; raising it does the opposite. Default 500. Floor-clamped to 1 (anything lower reverts to the default).
add_filter( 'perflocale/migration/wpml_string_batch_size', static fn(): int => 1000 );Parameters: int $size.
File: src/Migration/WpmlImporter.php
perflocale/migration/translatepress/gettext_batch_size
Rows fetched per keyset-paginated batch when importing gettext string translations from TranslatePress's per-language trp_gettext_* tables. Streams via WHERE g.original_id > $last_id ORDER BY g.original_id ASC LIMIT N; previously the importer used a flat LIMIT 10000 that both capped peak memory at ~5 MB per language AND silently truncated sites with more than 10k gettext strings per language. The keyset cursor removes the silent-truncation failure mode entirely; lowering the batch size further reduces memory at the cost of more SQL roundtrips. Default 1000. Floor-clamped to 100.
add_filter( 'perflocale/migration/translatepress/gettext_batch_size', static fn(): int => 2500 );Parameters: int $size.
File: src/Migration/TranslatePressImporter.php
perflocale/strings/scanner/max_file_bytes
Maximum size of a single PHP file the string scanner will read. Files larger than this are skipped to keep peak memory bounded. Default 2097152 (2 MB). Floor-clamped to 64 KB.
add_filter( 'perflocale/strings/scanner/max_file_bytes', static fn(): int => 10 * 1024 * 1024 ); // 10 MBParameters: int $bytes.
File: src/String/StringScanner.php
perflocale/strings/scanner/batch_size
Strings buffered in memory before flushing to the DB via bulk_insert. Default 500. Clamped to 50–5000. Callers that pass an explicit $batch_size to StringScanner::scan() override the filter; the filter is mostly useful when scanning is triggered via the StringScan background job which uses the default.
add_filter( 'perflocale/strings/scanner/batch_size', static fn(): int => 2000 );Parameters: int $size.
File: src/String/StringScanner.php
perflocale/jobs/stuck_timeout_seconds
How long a job may sit in queued or running without its updated_at being bumped before the daily GC declares it stuck and marks it failed. Default 6 hours.
Parameters: int $seconds.
File: src/Background/JobState.php
perflocale/jobs/pause_recheck_seconds
When the queue is paused, workers that pick up a job re-schedule it this many seconds later instead of running. Default 300 (5 minutes).
Parameters: int $seconds.
File: src/Background/WorkerRegistry.php
perflocale/jobs/pause_refresh_window
How long (seconds) a worker caches the queue’s paused/unpaused state in-process before re-reading it. Default 10. A chatty job emitting hundreds of progress ticks per second would otherwise force a full alloptions reload on every tick just to check the pause flag; this throttle window bounds that to one re-read per window. Lower it for faster pause response at the cost of more option reads; raise it to cut reads further. Floor-clamped to 1. The cache is keyed per blog, so a worker serving multiple blogs on multisite never returns another blog’s pause state.
Parameters: int $seconds (default 10, minimum 1).
File: src/Background/WorkerRegistry.php
perflocale/jobs/type_busy_retry_seconds
When the per-type concurrency lock is held by another worker, the new attempt re-queues this many seconds later. Default 60.
Parameters: int $seconds.
File: src/Background/WorkerRegistry.php
perflocale/jobs/type_busy_max_seconds
Upper bound on the delay between type-busy retries. When the per-type concurrency lock is contended, the re-queue delay grows exponentially with jitter (starting from type_busy_retry_seconds); this caps it so a long-contended type can’t back off to an absurd delay. Default 600s (10 min). This is the per-type analogue of lock_busy_max_seconds, which caps the per-job lock backoff.
add_filter( 'perflocale/jobs/type_busy_max_seconds', static fn(): int => 1800 ); // 30 min capParameters: int $seconds (default 600).
File: src/Background/WorkerRegistry.php
perflocale/jobs/lock_busy_max_retries
Maximum number of times a worker will re-queue itself when the per-job concurrency lock is held by another worker. After this many attempts the job is marked failed with a diagnostic message; the operator can manually retry from the Jobs admin page. Distinct from the type-busy retry above — this one trips when the same job_id is being processed by a sibling worker (typically a leaked lock row from a crashed worker that never released). Default 20.
// Tighter cap for tenants where a wedged lock should escalate to ops faster.
add_filter( 'perflocale/jobs/lock_busy_max_retries', static fn(): int => 5 );Parameters: int $max.
File: src/Background/WorkerRegistry.php
perflocale/jobs/lock_busy_max_seconds
Upper bound on the delay between lock-busy retries (the delay grows exponentially with jitter; this caps it). Default 600s (10 min).
add_filter( 'perflocale/jobs/lock_busy_max_seconds', static fn(): int => 1800 ); // 30 min capParameters: int $seconds.
File: src/Background/WorkerRegistry.php
perflocale/jobs/runner
Globally override the runner instance. For tests, custom deployments, or third-party schedulers (Sidekiq, SQS, etc.). Return a JobRunnerInterface instance to bypass the engine setting and Action Scheduler detection.
add_filter( 'perflocale/jobs/runner', static function ( $default ) {
return new MyCustomQueueRunner();
}, 99 );Parameters: JobRunnerInterface|null $override.
File: src/Background/JobRunnerFactory.php
Dev-mode guard: a return that is neither null nor a JobRunnerInterface instance triggers _doing_it_wrong() and the factory falls back to the engine setting / Action Scheduler auto-detect.
perflocale/glossary/max_csv_bytes
Maximum size of a glossary CSV upload accepted by the admin importer. Default 100 MB.
Parameters: int $bytes.
File: src/Admin/AdminController.php
perflocale/po/max_bytes
Maximum size of a .po file accepted by the admin PO importer (PerfLocale › Strings › Import). Larger uploads are rejected before parsing, because WordPress core's \PO parser accumulates every entry in memory. Gates the admin-UI upload only — the WP-CLI po-import path is not size-capped. Default 50 MB.
Parameters: int $bytes.
File: src/Admin/AdminController.php
perflocale/jobs/should_dispatch
Veto a job dispatch before it runs — the kill switch the per-type capability check can't express on its own. Returning false blocks the dispatch entirely (sync or async); the caller receives ['mode' => 'denied', 'error' => ...]. Returning a string vetoes with that string as the human-readable error message. Returning true (the default) lets the dispatch proceed unchanged. Distinct from perflocale/jobs/threshold/<type>, which only decides sync-vs-async — should_dispatch is yes/no.
// Block all jobs between Friday 17:00 UTC and Monday 09:00 UTC (deploy freeze).
add_filter( 'perflocale/jobs/should_dispatch', static function ( $proceed, \PerfLocale\Background\AbstractJob $job, array $args ) {
$dow = (int) gmdate( 'w' );
$hour = (int) gmdate( 'H' );
$frozen = ( $dow === 5 && $hour >= 17 ) || $dow === 6 || ( $dow === 0 ) || ( $dow === 1 && $hour < 9 );
return $frozen ? 'Deploy freeze in effect — queued operations resume Monday 09:00 UTC.' : $proceed;
}, 10, 3 );
// Per-tenant quota: cap bulk MT dispatches at 1000 source x target pairs per day.
add_filter( 'perflocale/jobs/should_dispatch', static function ( $proceed, $job, $args ) {
if ( $job->get_type() !== 'bulk_translate' ) { return $proceed; }
$today_count = (int) get_transient( 'perflocale_bulk_mt_quota_' . gmdate( 'Y-m-d' ) );
$cost = count( $args['source_ids'] ?? [] ) * count( $args['target_lang_ids'] ?? [] );
if ( $today_count + $cost > 1000 ) {
return 'Daily bulk-MT quota exhausted (1000 pairs/day).';
}
set_transient( 'perflocale_bulk_mt_quota_' . gmdate( 'Y-m-d' ), $today_count + $cost, DAY_IN_SECONDS );
return $proceed;
}, 10, 3 );Parameters:
bool $proceed— defaulttrue.AbstractJob $job— the job instance about to dispatch (call$job->get_type()to branch by type).array $args— the dispatch args (shape varies per job type).
Returns: bool|string. Return false to deny with the default error, or a non-empty string to deny with a custom error.
File: src/Background/Dispatcher.php
Reliability & Circuit Breakers
PerfLocale wraps every external dependency (MT provider, webhook receiver, FX API, geo-IP service, AI scoring) in a circuit breaker. After N consecutive failures within a sliding window, the breaker trips OPEN and subsequent calls short-circuit with a typed \PerfLocale\Concurrency\BreakerOpenException — no more piling retries onto a failing dependency, no more visitors waiting for a wedged service. Site Health surfaces every open breaker with a one-click reset; recovery is automatic when the upstream comes back. The hooks below tune trip / window / cooldown thresholds.
Breaker keys follow the pattern mt_<provider_id> (e.g. mt_deepl, mt_wp_ai_client), webhook_<uuid>, fx_sync, geo_<provider_id> (e.g. geo_ipapi_co), mt_quality_scoring.
perflocale/breaker/disabled
Global kill-switch. When this filter returns true, every breaker becomes a no-op — is_open() always returns false and record_failure() never trips anything. Use to temporarily disable circuit-breaker behaviour while debugging upstream issues, or to ship a hotfix that opts out without rolling back the plugin.
// Disable all breakers (debugging only — re-enable as soon as possible).
add_filter( 'perflocale/breaker/disabled', '__return_true' );Parameters: bool $disabled — Default false.
File: src/Concurrency/Breaker.php
perflocale/breaker/threshold
Number of failures within the rolling window that trips a breaker into OPEN. Global default 5. Per-key variant perflocale/breaker/threshold/<key> overrides for a specific breaker (e.g. perflocale/breaker/threshold/mt_deepl).
// Tighten threshold for the MT provider — trip after 2 failures.
add_filter( 'perflocale/breaker/threshold/mt_deepl', static fn(): int => 2 );
// Loosen the global default — tolerate more transient errors.
add_filter( 'perflocale/breaker/threshold', static fn(): int => 10 );Parameters: int $threshold — Default 5. Floor 1.
File: src/Concurrency/Breaker.php
perflocale/breaker/window_seconds
Length of the sliding window (seconds) within which the failure counter accumulates. A failure older than this resets the counter. Global default 300s (5 minutes). Per-key variant perflocale/breaker/window_seconds/<key>.
// Short window for webhook deliveries — only count failures in the last minute.
add_filter( 'perflocale/breaker/window_seconds/webhook_my-uuid', static fn(): int => 60 );Parameters: int $seconds — Default 300. Floor 1.
File: src/Concurrency/Breaker.php
perflocale/breaker/cooldown_seconds
How long the breaker stays in OPEN state before allowing a single probe call (HALF_OPEN). If the probe succeeds, the breaker closes; if it fails, it re-opens for another cooldown cycle. Global default 300s. Per-key variant perflocale/breaker/cooldown_seconds/<key>.
// Long cooldown for an unreliable third-party FX API — try once per hour.
add_filter( 'perflocale/breaker/cooldown_seconds/fx_sync', static fn(): int => HOUR_IN_SECONDS );
// Quick recovery for your own internal services.
add_filter( 'perflocale/breaker/cooldown_seconds/webhook_internal-uuid', static fn(): int => 30 );Parameters: int $seconds — Default 300. Floor 1.
File: src/Concurrency/Breaker.php
Dev-mode guard: a non-int return (on either the global or per-key form) triggers _doing_it_wrong() and the breaker falls back to the default 300s cooldown.
BreakerOpenException (catchable)
Not a hook but the typed companion to the breaker filters above. When a breaker is OPEN, AbstractProvider::make_request() throws this instead of firing the HTTP call. Catch it to route the request to a graceful-degradation path (cached translation, translation memory, "service unavailable" UI) without conflating with genuine downstream errors.
try {
$translated = $mt_service->translate_text( $text, 'en', 'de' );
} catch ( \PerfLocale\Concurrency\BreakerOpenException $e ) {
// Provider is being rate-limited / auth-rejected / 5xx'd.
// $e->get_breaker_key() returns "mt_deepl" or similar.
// $e->getMessage() is human-readable (includes the retry-in seconds).
$translated = my_fallback_translation_memory_lookup( $text );
} catch ( \RuntimeException $e ) {
// Genuine downstream error (not a breaker pre-emption) — log + skip.
}Class: \PerfLocale\Concurrency\BreakerOpenException extends \RuntimeException
File: src/Concurrency/BreakerOpenException.php
Export & Import
Extension points for the data-export / data-import lifecycle. Pair the export/sections filter with the matching import/section/<name> action to round-trip your addon's own data through wp perflocale export / import.
perflocale/export/sections
Register one or more top-level sections that your addon wants to add to the exported JSON envelope (siblings of data, settings, roles). Each entry in the returned array is written as "<section_name>": <json> at the envelope's top level. Reserved core keys (perflocale_export, version, format_version, exported_at, site_url, sections, settings, roles, data) cannot be overwritten and are silently dropped if a callback tries.
// Acme Reviews addon: ship its rows in every export.
add_filter( 'perflocale/export/sections', static function ( array $sections, array $context ): array {
global $wpdb;
$rows = $wpdb->get_results(
"SELECT id, post_id, language_id, body FROM {$wpdb->prefix}perflocale_addon_acme_reviews",
ARRAY_A
);
$sections['acme_reviews'] = [
'schema_version' => 2,
'rows' => $rows ?: [],
];
return $sections;
}, 10, 2 );Parameters:
array $sections— existing sections (empty by default). Keyed by section name; each value is JSON-serialisable.array $context—{ requested: string[], format_version: int }.requestedis the user-chosen section list (skip your section if it's not requested);format_versionis the envelope schema version (always1in v1.0).
Returns: array<string, mixed>.
File: src/Admin/DataExporter.php
Machine Translation (bulk)
perflocale/mt/bulk/before_translate
Short-circuit a single (source post, target language) row inside the bulk-translate job before the default TranslationService::translate_post() call. Mirrors WordPress's pre_* filter convention: return null (the default) to let the regular flow run; return any other value to skip the default call and treat the returned value as this row's result.
- Return
[ 'post_id' => int ]— counted as created. - Return any other array, string, or
false— counted as skipped (theafter_translateaction still fires, withshort_circuited => true). - Return
null— default flow runs.
// Skip MT on posts tagged "do-not-translate".
add_filter( 'perflocale/mt/bulk/before_translate', static function ( $pre, int $source_id, string $target_slug, array $ctx ) {
if ( has_term( 'do-not-translate', 'post_tag', $source_id ) ) {
return 'skip'; // counted as skipped, no provider call made
}
return $pre;
}, 10, 4 );
// Route Arabic translations through a different provider by claiming a result.
add_filter( 'perflocale/mt/bulk/before_translate', static function ( $pre, int $source_id, string $target_slug, array $ctx ) {
if ( $target_slug !== 'ar' ) { return $pre; }
$post_id = my_addon_translate_via_custom_arabic_provider( $source_id );
return $post_id ? [ 'post_id' => $post_id ] : 'skip';
}, 10, 4 );Parameters:
mixed $pre—nullby default.int $source_id— source post ID.string $target_slug— target language slug (e.g.de,ar-EG).array $context—{ source_id, target_slug, target_id, provider, processed, total }.provideris the configured MT provider key (deepl/google/ etc.).processed/totallet you log progress.
Returns: mixed (see semantics above).
File: src/Background/Jobs/BulkTranslateJob.php
perflocale/mt/bulk_string_max_per_dispatch
Hard ceiling on how many strings a single Bulk-Translate Strings dispatch (Strings admin page → bulk MT toolbar) will touch. Default 5000. The cap applies to all three selection modes — an explicit ID list, a filtered set, or all strings — so a misclick on a 100k-row table can’t turn into one runaway provider bill. Raise it only if you genuinely intend to translate more strings in one run, and have the provider quota/budget for it.
This is not the same as the per-job Auto-mode cutoff — that’s perflocale/jobs/threshold/<type>, which decides whether a dispatch runs inline or async based on args_size() (string_count × target_count). This filter instead bounds how many strings the dispatch resolves in the first place. There is no UI/settings field for this ceiling; it is code-only.
// Roomy host with budget: allow up to 20,000 strings per bulk dispatch.
add_filter( 'perflocale/mt/bulk_string_max_per_dispatch', fn() => 20000 );Parameters:
int $cap— the ceiling,5000by default. The return value is cast toint.
Returns: int — the maximum number of strings a single dispatch may translate.
File: src/Background/Jobs/BulkStringTranslateJob.php
Actions
Plugin Lifecycle
perflocale/activated
Fires after the plugin is activated.
Parameters: string $version - Plugin version.
File: src/Activator.php
perflocale/activation/chunk_size
Sites fetched per iteration during multisite network activation. PerfLocale activates each subsite in chunks so a network with tens of thousands of sites doesn't load every row at once. Lower this on memory-constrained PHP workers; raise it if you've benchmarked your activation hook and want fewer round-trips.
add_filter( 'perflocale/activation/chunk_size', fn() => 25 );Parameters: int $chunk - Default 100. Floored at 1.
File: perflocale.php
perflocale/deactivated
Fires after the plugin is deactivated.
File: src/Deactivator.php
perflocale/loaded
Fires after PerfLocale is fully bootstrapped and all services are registered.
add_action( 'perflocale/loaded', function ( \PerfLocale\Plugin $plugin ): void {
// Safe to use all PerfLocale services here.
$router = $plugin->get( 'router' );
} );Parameters: Plugin $plugin - The plugin container instance.
File: src/Bootstrap.php
perflocale/upgraded
Fires after a database schema migration completes.
Parameters:
string $old_version- Previous schema version.string $new_version- New schema version.
Args: 2
File: src/Database/Migrator.php
perflocale/updated
Fires after the plugin code version changes (e.g., updated from 1.0.0 to 1.1.0). Unlike perflocale/upgraded which fires on DB schema changes, this fires on any code version bump. Useful for version-specific non-DB tasks like migrating settings or regenerating files.
add_action( 'perflocale/updated', function ( string $old_version, string $new_version ): void {
if ( version_compare( $old_version, '1.1.0', '<' ) ) {
// One-time task for 1.1.0 update.
}
}, 10, 2 );Parameters:
string $old_version- Previous plugin version.string $new_version- Current plugin version.
Args: 2
File: src/Database/Migrator.php
perflocale/strings/after_scan
Fires after the "Scan for Strings" action completes. Addons hook here to register non-gettext translatable strings (e.g., WooCommerce email subjects, attribute labels).
add_action( 'perflocale/strings/after_scan', function (): void {
// Register custom non-gettext strings here.
} );Args: 0
File: src/Admin/AdminController.php
Language Events
perflocale/language/detected
Fires after the current language is detected from the request.
add_action( 'perflocale/language/detected', function ( string $slug, string $method ): void {
// $method is 'url', 'cookie', 'browser', or 'default'.
if ( $method === 'browser' ) {
// First-time visitor, language detected from browser.
}
}, 10, 2 );Parameters:
string $slug- Detected language slug.string $method- Detection method used.
File: src/Router/LanguageRouter.php
perflocale/language/added
Fires after a new language is added.
Parameters: object $language - The language object.
File: src/Database/Repository/LanguageRepository.php
perflocale/language/updated
Fires after a language is updated.
Parameters:
object $language- Updated language object.array $old- Previous language data.
File: src/Database/Repository/LanguageRepository.php
perflocale/language/deleted
Fires after a language is deleted.
Parameters:
int $id- Language ID.string $slug- Language slug.
File: src/Database/Repository/LanguageRepository.php
perflocale/default_language/changed
Fires after the default language is changed.
Parameters:
object $new_default- New default language.object $old_default- Previous default language.
File: src/Database/Repository/LanguageRepository.php
perflocale/language/slug_renamed
Fires after a language slug is renamed (e.g. en → en-us). Old URLs are 301-redirected to the new slug; this hook lets integrations sync downstream caches, search indices, or external systems that referenced the old slug.
Parameters:
int $id- Language row ID.string $old_slug- Previous slug.string $new_slug- Renamed slug.
File: src/Database/Repository/LanguageRepository.php
perflocale/languages/reordered
Fires after the Languages-list drag-and-drop reorder UI persists a new sort_order for one or more rows.
Parameters:
array $ordered_ids- Language IDs in their new visual order.int $offset- Starting position (0-based) — sort_order values run$offset + 1 … $offset + count($ordered_ids). Lets the caller renumber a paginated slice without disturbing rows on other pages.
File: src/Database/Repository/LanguageRepository.php
Translation Events
perflocale/translation/created
Fires after a translation post or term is created.
add_action( 'perflocale/translation/created', function ( int $new_id, string $type, string $target_slug, int $source_id ): void {
if ( $type === 'post' ) {
// Correlate source → translation, e.g. send to CRM / analytics.
my_crm_record_translation_pair( $source_id, $new_id, $target_slug );
}
}, 10, 4 );Parameters:
int $new_id- New post/term ID.string $type- Object type ('post'or'term').string $target_slug- Target language slug.int $source_id- Source post/term ID the translation was created from. Useful for correlating analytics, CRM entries, or webhook payloads without a secondary translation-group lookup.
Args: 4
File: src/Translation/PostTranslationManager.php, src/Translation/TermTranslationManager.php
perflocale/translation/meta_copy_failed
Fires when one or more of the post-meta / featured-image / taxonomy-term copy steps throws while creating a translation. The translation post and its group link are still created — a linked translation with missing meta is recoverable via re-sync, whereas rolling back would lose the translator’s work — so this is a notification signal, not an error you need to recover from. The same failures are also written to the _perflocale_meta_copy_errors post meta (surfaced as a per-post admin notice); this hook lets integrators forward the signal to Slack, email, or an error tracker.
add_action( 'perflocale/translation/meta_copy_failed', function ( int $new_post_id, int $source_id, string $target_slug, array $errors ): void {
foreach ( $errors as $err ) {
// $err = [ 'step' => 'post_meta'|'featured_image'|'taxonomy_terms', 'message' => string ]
error_log( sprintf(
'PerfLocale copy step "%s" failed for translation %d (source %d, %s): %s',
$err['step'], $new_post_id, $source_id, $target_slug, $err['message']
) );
}
}, 10, 4 );Parameters:
int $new_post_id— the newly-created translation post ID.int $source_id— the source post the translation was created from.string $target_slug— target language slug.array $errors— one entry per failed step:{ step: 'post_meta'|'featured_image'|'taxonomy_terms', message: string }.
Args: 4
Since: 1.0.1
File: src/Translation/PostTranslationManager.php
perflocale/translation/linked
Fires when an object is linked to a translation group.
Parameters:
int $group_id- Translation group ID.int $object_id- Object ID.int $language_id- Language ID.
File: src/Database/Repository/TranslationGroupRepository.php
perflocale/translation/status_changed
Fires when a translation's status changes (e.g., draft to published).
Parameters:
int $object_id- Object ID.string $status- New status.int $language_id- Language ID.
File: src/Database/Repository/TranslationGroupRepository.php
perflocale/content/changed
Fires when source content changes, marking translations as potentially outdated.
Parameters:
int $object_id- Object ID.string $type- Object type value.int $group_id- Translation group ID.
File: src/Translation/ContentChangeDetector.php
Settings
perflocale/settings/updated
Fires after plugin settings are saved.
Parameters:
array $merged- Merged (new) settings array.array $current- Previous settings array.
File: src/Settings.php
Workflow
perflocale/workflow/assigned
Fires when a translation is assigned to a user.
Parameters:
int $object_id- Post/term ID.string $object_type- Object type.int $language_id- Language ID.int $user_id- Assigned user ID.
File: src/Admin/TranslatorRole.php
perflocale/workflow/status_changed
Fires when a workflow status changes.
add_action( 'perflocale/workflow/status_changed', function ( int $object_id, string $type, int $lang_id, string $status ): void {
if ( $status === 'approved' ) {
// Send Slack notification.
}
}, 10, 4 );Parameters:
int $object_id- Post/term ID.string $object_type- Object type.int $language_id- Language ID.string $status- New workflow status.
File: src/Admin/TranslatorRole.php
perflocale/workflow/bulk_updated
Fires after the Assignments-page bulk-actions UI updates one column on a batch of workflow rows. Use this to mirror status / priority / assignee / deadline changes into external project-management tools.
add_action( 'perflocale/workflow/bulk_updated', function ( array $ids, string $column, $value ): void {
if ( $column === 'status' && $value === 'approved' ) {
// Notify Slack that N rows were just bulk-approved.
}
}, 10, 3 );Parameters:
array $ids- Workflow row IDs that were updated.string $column- Column updated: one ofstatus,priority,assigned_to,deadline.mixed $value- Sanitised new value written to the column.
File: src/Admin/TranslatorRole.php
perflocale/workflow/bulk_deleted
Fires after the Assignments-page bulk Delete action removes rows.
Parameters:
array $ids- Workflow row IDs that were deleted.
File: src/Admin/TranslatorRole.php
perflocale/translations/bulk_marked_needs_update
Fires after the Translations-page bulk “Mark as Needs Update” action flips translation_links.status to needs_update for one or more (source_post, target_lang) pairs. Use this to dispatch translator notifications or push staleness flags to external workflow systems.
Parameters:
array $source_ids- Source post IDs.array $target_lang_ids- Target language IDs (single ID or all active languages).int $count- Number oftranslation_linksrows actually updated.
File: src/Admin/AdminController.php
IndexNow (1.0.0)
perflocale/indexnow/push_result
Fires after every IndexNow push attempt (per-host). Because wp_remote_post runs with blocking => false, $response reflects the dispatch result (usually a truncated WP_Error or an array with no body), not the final HTTP status. Use this to log pushes, track deliveries, or count daily volume against search-engine rate limits.
add_action( 'perflocale/indexnow/push_result', function ( $response, string $endpoint, string $host, array $urls, array $context ): void {
error_log( sprintf(
'[IndexNow] %s host=%s urls=%d trigger=%s',
is_wp_error( $response ) ? 'FAIL: ' . $response->get_error_message() : 'OK',
$host,
count( $urls ),
(string) ( $context['trigger_post_id'] ?? '' )
) );
}, 10, 5 );Parameters: WP_Error|array $response, string $endpoint, string $host, array $urls, array $context.
File: src/Seo/IndexNowPusher.php
Machine Translation
perflocale/mt/quality_score_persist
Fires after the MT quality-scoring job persists a 1-5 score to a row. Hook this to mirror scores into external observability (Datadog, ELK, Slack), auto-assign low scorers to a senior translator through your workflow integration, or trigger ad-hoc notifications.
add_action( 'perflocale/mt/quality_score_persist', function ( int $score, object $row, string $table ): void {
if ( $score <= 2 ) {
// Ping our review channel.
MyNotifier::low_score( $score, $row, $table );
}
}, 10, 3 );Parameters:
int $score- Score that was just stored (1-5).object $row- Row that was scored. Shape varies by$table.string $table-'string_translations'or'translation_links'.
File: src/Background/Jobs/MtQualityScoreJob.php
perflocale/machine_translation/before
Fires before machine translation starts for a post.
Parameters:
int $post_id- Post ID.string $provider_id- Provider identifier.
File: src/MachineTranslation/TranslationService.php
perflocale/machine_translation/after
Fires after machine translation completes successfully.
Parameters:
int $post_id- Post ID.string $provider_id- Provider identifier.mixed $result- Translation result.
File: src/MachineTranslation/TranslationService.php
perflocale/machine_translation/failed
Fires when machine translation fails with an exception.
Parameters:
int $post_id- Post ID.string $provider_id- Provider identifier.Exception $exception- The exception.
File: src/MachineTranslation/TranslationService.php
Cache
perflocale/cache/flush_all
Fires after the entire plugin cache is flushed.
File: src/Cache/CacheManager.php
perflocale/cache/flush_object
Fires after cache is flushed for a specific object.
Parameters:
int $object_id- Object ID.string $object_type- Object type.
File: src/Cache/CacheManager.php
Background Jobs
Lifecycle actions for the background-processing system. See the Background Jobs doc for the full feature reference.
perflocale/jobs/enqueued
Fires after a job has been successfully enqueued for async execution.
add_action( 'perflocale/jobs/enqueued', static function ( string $job_id, string $type, string $engine, array $args ): void {
error_log( "PerfLocale enqueued $type job $job_id on $engine" );
}, 10, 4 );Parameters:
string $job_id— UUID v4 of the new job.string $type— job type slug (e.g.string_scan,data_import).string $engine— runner engine:action_schedulerorwp_cron.array $args— the dispatch args.
File: src/Background/Dispatcher.php
perflocale/jobs/completed
Fires after a worker finishes a job successfully.
add_action( 'perflocale/jobs/completed', static function ( string $job_id, string $type, array $result ): void {
// Push to your metrics pipeline...
}, 10, 3 );Parameters: string $job_id, string $type, array $result (worker's return value, already stored on the job row, truncated to MAX_RESULT_BYTES = 64 KB).
File: src/Background/WorkerRegistry.php
perflocale/jobs/failed
Fires when a worker throws an exception and the retry-with-backoff is about to be scheduled (or skipped because the attempt cap has been hit). Receives the FULL untruncated throwable, useful for monitoring — the version persisted to the job row is path-redacted and truncated.
add_action( 'perflocale/jobs/failed', static function ( string $job_id, string $type, \Throwable $e ): void {
sentry_capture_exception( $e, [
'tags' => [ 'perflocale_job_id' => $job_id, 'perflocale_job_type' => $type ],
] );
}, 10, 3 );Parameters: string $job_id, string $type, \Throwable $e.
File: src/Background/WorkerRegistry.php
perflocale/jobs/canceled
Fires when a long-running worker cooperatively aborts itself in response to an operator cancel mid-flight. Distinct from perflocale/jobs/failed because it isn't an error — useful for distinguishing operator-canceled from worker-errored in dashboards.
Parameters: string $job_id, string $type.
File: src/Background/WorkerRegistry.php
Export & Import
Lifecycle hooks for the data-export / data-import flow. Pair with the Export & Import filters to round-trip addon-shipped data through wp perflocale export/import.
perflocale/export/written
Fires inside DataExportJob::execute() after the export file has been successfully written to disk, BEFORE the job result is stored on the JobState row. The file is still readable at $path when this action fires — it will be served (and deleted) later by the download endpoint when the operator clicks Download on PerfLocale → Jobs.
Most common use: offsite-backup hooks (copy the export to S3 / Dropbox / a remote shell) and audit-log entries. Also fires when the export ran inline (below the threshold) and was streamed directly — in that case $path is the inline-streamed temp file and may be unlinked immediately after this action returns.
// Mirror every successful site export to S3.
add_action( 'perflocale/export/written', static function ( string $path, int $bytes, array $sections ): void {
if ( $bytes === 0 ) { return; }
my_addon_s3_upload(
$path,
's3://acme-backups/perflocale/' . gmdate( 'Y/m/d/' ) . basename( $path )
);
}, 10, 3 );Parameters:
string $path— absolute path to the written export file (always insideuploads/perflocale-exports/).int $bytes— size of the written file.array<int,string> $sections— the section keys that were exported (settings,glossary,translations, etc.).
File: src/Background/Jobs/DataExportJob.php
perflocale/export/download/before_serve
Fires after every gate (nonce, capability, job-status, realpath) has passed and the export file is about to be streamed to the operator's browser, BEFORE any HTTP header is sent. Use for compliance / audit-log entries ("user X downloaded export Y at Z") and monitoring-pipeline events. Do not echo from callbacks here — output would land in the response body before the JSON download.
add_action( 'perflocale/export/download/before_serve', static function ( string $job_id, string $path, array $job ): void {
my_audit_log( [
'event' => 'export_download',
'user_id' => get_current_user_id(),
'job_id' => $job_id,
'size' => filesize( $path ),
] );
}, 10, 3 );Parameters: string $job_id, string $real_path, array $job (the JobState row).
File: src/Admin/AdminController.php
perflocale/export/download/after_serve
Fires AFTER the export file has been streamed to the browser and BEFORE the single-use deletion takes effect. The file is still on disk at $real_path when this action fires — this is the last chance to act on it. Useful for chaining a post-download workflow (queue an integrity-check job that re-reads the file, kick off offsite-backup, push a webhook). Don't echo — the HTTP body is already being sent.
// Queue an integrity-check job on the freshly-served export.
add_action( 'perflocale/export/download/after_serve', static function ( string $job_id, string $path, array $job, int $size ): void {
\PerfLocale\Background\BackgroundEvents::enqueue(
'my_addon_verify_export_hash',
[ $path, hash_file( 'sha256', $path ) ],
0
);
}, 10, 4 );Parameters: string $job_id, string $real_path, array $job, int $size (bytes served, 0 when filesize() failed).
File: src/Admin/AdminController.php
perflocale/import/section/<name>
Fires once for every top-level key in the import envelope that isn't a core section (settings, roles, data, etc.). The action name is dynamic — <name> is the section key your addon used in the matching export filter. Use the same key on both sides so round-tripping works automatically.
// Acme Reviews addon: restore its own rows during an import.
add_action( 'perflocale/import/section/acme_reviews', static function ( $section_data, array $ctx ): void {
global $wpdb;
$rows = is_array( $section_data ) ? ( $section_data['rows'] ?? [] ) : [];
if ( $ctx['replace'] ) {
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}perflocale_addon_acme_reviews" );
}
foreach ( $rows as $r ) {
$wpdb->insert( "{$wpdb->prefix}perflocale_addon_acme_reviews", $r );
}
}, 10, 2 );Parameters:
mixed $section_data— the decoded JSON payload your addon wrote to this section. Same shape the export filter returned.array $context—{ replace: bool, format_version: int, file_path: string }.replacemirrors the import's replace-mode flag; truncate your tables when it'strue.file_pathis the path of the source envelope.
File: src/Admin/DataImporter.php
perflocale/import/completed
Fires after DataImporter::import() has finished restoring rows and flushing caches, with the final result stats. Fires for BOTH sync and async (data_import job) code paths. Use for cache invalidation, audit logging, IndexNow pushes, or Slack notifications.
add_action( 'perflocale/import/completed', static function ( array $result, string $file_path, bool $replace ): void {
if ( ! empty( $result['errors'] ) ) { return; }
wp_remote_post( 'https://hooks.slack.com/...', [
'body' => wp_json_encode( [
'text' => sprintf(
'PerfLocale import done: %d rows imported, %d skipped (replace=%s).',
$result['imported'],
$result['skipped'],
$replace ? 'yes' : 'no'
),
] ),
] );
}, 10, 3 );Parameters: array $result ({ imported: int, skipped: int, errors: string[] }), string $file_path, bool $replace.
File: src/Admin/DataImporter.php
Machine Translation (bulk)
perflocale/mt/bulk/after_translate
Fires after every (source post, target language) row inside the bulk-translate job, whether the translation succeeded, failed, or was short-circuited by before_translate. Use for per-row observability: metrics, monitoring pipelines, workflow events.
// Per-row APM metric for bulk MT.
add_action( 'perflocale/mt/bulk/after_translate', static function ( int $source_id, string $target_slug, array $result, array $ctx ): void {
my_apm_increment( 'perflocale.mt.bulk.row', [
'provider' => $ctx['provider'] ?: 'unknown',
'target' => $target_slug,
'outcome' => ! empty( $result['post_id'] ) ? 'created' : ( $ctx['short_circuited'] ? 'skipped_filter' : 'failed' ),
'short_circuited' => $ctx['short_circuited'] ? '1' : '0',
] );
}, 10, 4 );Parameters:
int $source_id— source post ID.string $target_slug— target language slug.array $result— TranslationService result (['post_id' => int]on success; empty array on failure).array $context— same shape as thebefore_translatecontext plus two extra keys:short_circuited: bool— true if the row was handled by abefore_translatefilter callback.error: string— the exception message or fallback error text; empty string on success.
File: src/Background/Jobs/BulkTranslateJob.php
Addons
perflocale/addon/activated
Fires when an addon is activated (booted).
Parameters: string $addon_id - Addon identifier.
File: src/Addon/AddonRegistry.php
perflocale/addons/loaded
Fires after all addons have been loaded.
File: src/Addon/AddonRegistry.php
perflocale/addon/seeded
Fires once, immediately after the registry writes an addon's declared default settings on its first successful boot (the auto-seed pass — see Developer Toolkit → Settings). Useful for one-shot first-activation work: welcome email, sample-data import, telemetry opt-in prompt. Will NOT fire on subsequent boots even if the user clears all settings — re-firing requires an explicit AddonSettings::forget($id).
add_action( 'perflocale/addon/seeded', function ( string $addon_id, array $defaults ): void {
if ( $addon_id !== 'my-addon' ) {
return;
}
// Schedule the welcome email once.
wp_schedule_single_event( time() + 60, 'myaddon/welcome_email' );
}, 10, 2 );Parameters:
string $addon_id— Addon'sget_id()value.array $defaults— The defaults that were seeded.
File: src/Addon/AddonRegistry.php
perflocale/addon/settings/before_save
Fires inside the storage lock, immediately BEFORE the perflocale_addon_settings option commits a new entry for an addon. Use for pre-save auditing, validation logging, or cache invalidation that needs to happen while both old and new values are still visible.
Reentrancy: the write lock is non-reentrant. Calling AddonSettings::set() / set_addon() / forget() from a listener will time out at 10 seconds and return false. For cross-addon write reactions, hook perflocale/addon/settings/after_save from a wp_schedule_single_event deferred callback so your write happens outside the lock window.
add_action( 'perflocale/addon/settings/before_save', function ( string $addon_id, array $new, array $old ): void {
error_log( sprintf(
'addon=%s changed=%s',
$addon_id,
wp_json_encode( array_diff_assoc( $new, $old ) )
) );
}, 10, 3 );Parameters:
string $addon_id— Addon being saved.array $new_entry— The values about to commit.array $old_entry— The pre-save values (empty array if no entry existed).
File: src/Addon/AddonSettings.php
perflocale/addon/settings/after_save
Fires inside the storage lock, immediately AFTER the option commits. Same reentrancy caveat as before_save. This is the right hook for "addon A reacts to addon B's settings change" via a deferred dispatch:
add_action( 'perflocale/addon/settings/after_save', function ( string $addon_id, array $new, array $old ): void {
if ( $addon_id !== 'translation-memory' ) {
return;
}
// Schedule a cache rebuild OUTSIDE the lock window.
wp_schedule_single_event( time() + 5, 'myaddon/rebuild_tm_cache' );
}, 10, 3 );Neither hook fires on rejected writes (invalid addon id, over the 16 KiB per-addon size cap, or lock acquisition failure). If you need to observe rejections, wrap the call site and check the bool return.
Parameters: identical to before_save.
File: src/Addon/AddonSettings.php
WooCommerce
perflocale/woocommerce/exchange_rates_synced
Fires after exchange rates are successfully fetched and saved.
Parameters:
array $rates- Currency code => exchange rate.string $base_currency- WooCommerce base currency code.string $provider_id- Provider that was used.
File: src/WooCommerce/ExchangeRateSync.php
perflocale/woocommerce/inventory_synced
Fires after inventory fields are synced across language variants.
Parameters:
int $product_id- Source product ID.array $synced_ids- IDs of sibling translations that were updated.array $fields- Meta keys that were synced.
File: src/WooCommerce/InventorySync.php
GeoIP Redirect
perflocale/geo/redirected
Fires after a GeoIP redirect has been performed.
Parameters:
string $language_slug- The language the visitor was redirected to.string $country_code- Detected country code.string $ip- Visitor IP address.
File: src/Router/GeoRedirect.php
Edge Integration (filters)
perflocale/edge/enabled
Programmatic override for whether the edge-integration feature is active. When returned true, PerfLocale publishes /wp-json/perflocale/v1/config and honours the edge_hint detection method, regardless of the edge_integration_enabled setting value.
add_filter( 'perflocale/edge/enabled', '__return_true' );Parameters: bool $enabled - Current effective state.
File: src/Settings.php
perflocale/edge/hint_header
Rename the HTTP header used to carry the edge-selected language.
add_filter( 'perflocale/edge/hint_header', fn() => 'X-Vercel-Lang' );Parameters: string $header_name - Default X-PerfLocale-Lang.
File: src/Router/LanguageRouter.php
perflocale/edge/hint_cookie
Rename the cookie used as fallback for edge-selected language.
add_filter( 'perflocale/edge/hint_cookie', fn() => 'my_lang_cookie' );Parameters: string $cookie_name - Default perflocale_edge_lang.
File: src/Router/LanguageRouter.php
perflocale/edge/accept_hint
Veto a specific edge-hint per request. Return false to reject an otherwise valid hint (e.g. behind a reverse proxy that mis-forwards headers).
add_filter( 'perflocale/edge/accept_hint', function ( bool $accept, string $slug ): bool {
if ( ! empty( $_SERVER['HTTP_X_INTERNAL_PROBE'] ) ) {
return false;
}
return $accept;
}, 10, 2 );Parameters:
bool $accept- Defaulttrue.string $slug- Candidate language slug from header/cookie.
File: src/Router/LanguageRouter.php
Garbage Collection & Retention
Fire-and-observe actions emitted at the end of each GC pass. Use them for monitoring (Datadog, New Relic), audit trails (“N strings GC’d on YYYY-MM-DD”), or chained cleanups. None of the callbacks block or alter the GC behaviour. See the matching tuning filters under Garbage Collection & Retention in the Filters section.
perflocale/strings/gc_complete
Fires after the daily mark-and-sweep GC deletes stale rows from perflocale_strings (and the cascade from perflocale_string_translations). Only fires when at least one row was deleted — quiet ticks don’t emit. Use it for observability when shipping a wp.org plugin update, so you can confirm the GC actually fired after deploy.
add_action( 'perflocale/strings/gc_complete', static function ( int $strings_deleted, int $translations_deleted ): void {
if ( function_exists( 'datadog_metric' ) ) {
datadog_metric( 'perflocale.gc.strings.deleted', $strings_deleted );
datadog_metric( 'perflocale.gc.string_translations.deleted', $translations_deleted );
}
}, 10, 2 );Parameters:
int $strings_deleted— rows deleted fromperflocale_strings.int $translations_deleted— rows deleted fromperflocale_string_translationsvia the cascade.
File: src/Database/Repository/StringRepository.php
perflocale/string_translations/orphans_swept
Fires after the defensive orphan-sweep in the daily GC deletes perflocale_string_translations rows whose parent strings.id no longer exists. Only fires when at least one orphan was found — a healthy install should never trigger this hook, so any callback firing is a signal that some code path bypassed the normal cascade.
// Alert when the orphan-sweep actually finds anything — suggests a buggy
// cascade somewhere we should investigate.
add_action( 'perflocale/string_translations/orphans_swept', static function ( int $deleted ): void {
error_log( '[PerfLocale] string_translations orphan-sweep deleted ' . $deleted . ' rows — investigate.' );
} );Parameters: int $deleted — number of orphan rows deleted.
File: src/Database/Repository/StringTranslationRepository.php
perflocale/tm/gc_complete
Fires after the weekly TM LRU GC evicts rows from perflocale_translation_memory. Only fires when at least one row was evicted (i.e. the table was over perflocale/tm/gc_row_cap). Use it for capacity-planning telemetry — a hook that fires every week tells you the cap is too low for your write rate.
add_action( 'perflocale/tm/gc_complete', static function ( int $deleted, int $total_before ): void {
// Track high-water marks so we know when to raise the cap.
update_option( 'myplugin/perflocale_tm_highwater', max(
(int) get_option( 'myplugin/perflocale_tm_highwater', 0 ),
$total_before
), false );
}, 10, 2 );Parameters:
int $deleted— rows evicted in this pass.int $total_before— row count BEFORE the eviction (useful for capacity-planning).
File: src/Translation/TranslationMemory.php
CDN Cache-Tag Headers (filters + action)
perflocale/cache_tags/enabled
Programmatic override for Cache-Tag header emission. Returning false kills the feature without touching the setting.
Parameters: bool $enabled - Current effective state.
File: src/Settings.php
perflocale/cache_tags/header_name
Change the response-header name. Useful for Fastly (Surrogate-Key) or custom CDNs.
add_filter( 'perflocale/cache_tags/header_name', fn() => 'Surrogate-Key' );Parameters: string $name - Default Cache-Tag.
File: src/Frontend/CacheTagEmitter.php
perflocale/cache_tags/tags
Modify the list of tags emitted for the current request. Tags are ASCII-only ([A-Za-z0-9\-_:.]), truncated per-entry to 128 chars and capped at 32 per response.
add_filter( 'perflocale/cache_tags/tags', function ( array $tags ): array {
$tags[] = 'theme:' . get_template();
return $tags;
} );Parameters: array $tags - Sanitised tag list.
File: src/Frontend/CacheTagEmitter.php
perflocale/cache_tags/max_header_length
Response-header byte budget (default 8000). Tags overflowing the budget are dropped silently.
Parameters: int $max - Default 8000.
File: src/Frontend/CacheTagEmitter.php
perflocale/cache/flush_all (action)
Fires after PerfLocale flushes its full cache layer (e.g. on language add/remove or admin "Clear cache" button). Hook this to drop your CDN’s entire zone or a wildcard tag. PerfLocale itself does not issue remote purges - that’s left to integrators (Cloudflare, Bunny, Fastly).
add_action( 'perflocale/cache/flush_all', function (): void {
my_cdn_purge_zone();
} );Parameters: none
File: src/Cache/CacheManager.php
perflocale/cache/flush_object (action)
Fires after PerfLocale invalidates a single object’s cache (post or term). Pair with the cache_tags/tags filter to map the object to your CDN tags and issue a targeted purge.
add_action( 'perflocale/cache/flush_object', function ( int $object_id, string $object_type ): void {
$tag = $object_type . ':' . $object_id;
my_cdn_purge_tags( [ $tag ] );
}, 10, 2 );Parameters: int $object_id, string $object_type (post or term).
File: src/Cache/CacheManager.php
perflocale/cache/flush_slugs (action)
Fires after PerfLocale flushes the translated-slug cache layer — currently invoked on language deletion (the slug_translations rows for the removed language are deleted alongside the static L1 memo, the persistent L2 group, the L3 transient envelopes, and the perflocale_has_any_slugs autoloaded zero-state flag). Hook this when your CDN or edge cache holds per-slug routing data that needs to be invalidated on the same event — e.g. a custom URL-shortener or a slug-keyed redirect table at the edge. PerfLocale does not issue remote purges itself; that’s left to integrators.
add_action( 'perflocale/cache/flush_slugs', function (): void {
my_cdn_purge_tags( [ 'perflocale:slugs' ] );
} );Parameters: none
File: src/Cache/CacheManager.php
perflocale/cache/flush_archive_hreflang (action)
Fires when an archive’s hreflang cache is invalidated for a given post (typically after the post’s status / language assignment changes).
add_action( 'perflocale/cache/flush_archive_hreflang', function ( int $post_id ): void {
my_cdn_purge_tags( [ 'archive-hreflang:' . $post_id ] );
} );Parameters: int $post_id.
File: src/Cache/CacheInvalidator.php
SEO Schema Enrichment
perflocale/seo/schema_enrichment_enabled
Programmatic override for JSON-LD schema enrichment across the six built-in SEO addons (Yoast, AIOSEO, Rank Math, SEOPress, Slim SEO, The SEO Framework). Default is true; return false to suppress inLanguage + workTranslation additions.
Parameters: bool $enabled - Current effective state.
File: src/Settings.php
Workflow Email
perflocale/workflow/email
Filter the outgoing workflow email before wp_mail() dispatches it. Useful for routing admin mail through a transactional provider, rewriting recipients, or appending localized footers.
add_filter( 'perflocale/workflow/email', function ( array $mail, WP_User $user ): array {
$mail['headers'][] = 'X-Transactional: workflow';
$mail['body'] .= "\n\n" . __( 'This is an automated notification.', 'my-theme' );
return $mail;
}, 10, 2 );Parameters:
array $mail-{ to, subject, body, vars }.varsis the rendered placeholder map:{post_title},{assignee_name},{language},{status},{priority},{deadline},{deadline_relative},{edit_url},{site_name}. See the Workflow placeholder reference for descriptions.WP_User $user- Recipient.
File: src/Admin/WorkflowNotifier.php
perflocale/workflow/bulk_reassign_digest_body
Filter the plaintext body of the digest email sent to a user when the Assignments-page bulk Reassign action routes a batch of assignments to them. One email per recipient is dispatched (not N), and this filter sees the assembled summary before wp_mail() ships it.
add_filter( 'perflocale/workflow/bulk_reassign_digest_body', function ( string $body, int $user_id, array $rows ): string {
// Append a CTA + signature.
$body .= "\n— Translation team\n";
return $body;
}, 10, 3 );Parameters:
string $body- Plaintext email body, including the “N new assignments” intro and one bullet per(post, language)pair with priority + deadline annotations.int $user_id- Recipient user ID (the new assignee).array $rows- Workflow rows being reassigned, captured BEFORE the bulk update so the body can mention prior state if needed.
File: src/Admin/AdminController.php
Rate Limits & Scan Caps
perflocale/mt/rate_limit
Per-user hourly ceiling on POST /perflocale/v1/machine-translate calls. Return 0 to disable rate limiting entirely.
add_filter( 'perflocale/mt/rate_limit', fn() => 1000 );Parameters: int $limit - Default 500 requests per hour.
File: src/Api/MachineTranslateController.php
perflocale/mt/rate_limit_site
Site-wide hourly ceiling on machine-translation requests, summed across every user. Pairs with perflocale/mt/rate_limit (per-user). The per-user and site counters share a single global lock so a hostile editor with the MT capability can't fan out parallel requests to drain the site-wide budget faster than the rate-limit check can register them.
// Tighten the global ceiling for a shared budget across an editorial team.
add_filter( 'perflocale/mt/rate_limit_site', fn() => 2000 );
// Disable the site cap entirely (per-user cap still applies).
add_filter( 'perflocale/mt/rate_limit_site', fn() => 0 );Parameters: int $limit - Default 5000 requests per hour, summed across all users. Set to 0 to disable the site cap.
Files: src/Api/MachineTranslateController.php, src/Api/BlockTranslateController.php
perflocale/glossary/scan_limit
Maximum posts scanned per invocation of the auto-glossary candidate scanner. Clamped to [10, 5000].
add_filter( 'perflocale/glossary/scan_limit', fn() => 2000 );Parameters: int $limit - Default 500.
File: src/Translation/GlossaryScanner.php
perflocale/import/quality_limits
Tune the data-quality gate that every import passes BEFORE any database write. The gate rejects invalid UTF-8, null bytes, cardinality bombs, oversized values, and malformed rows — errors name the exact JSON path so the operator can fix the file.
// Agency importing a 900-language terminology corpus.
add_filter( 'perflocale/import/quality_limits', fn() => [
'max_languages' => 1000,
'max_rows_per_table' => 2000000,
'max_value_bytes' => 4 * 1048576,
] );Parameters: array $limits - Keys: max_languages (default 500), max_rows_per_table (default 500,000), max_value_bytes (default 1,048,576).
File: src/Admin/DataImporter.php
Environment Gates
perflocale/dispatch/allow_non_production
Outward-facing dispatches (webhooks, IndexNow pings) fail closed on non-production environments: staging and development clones carry the production webhook URLs and IndexNow key in their cloned database, so without the gate a clone silently fires production endpoints and submits clone URLs to search engines. Managed hosts (WP Engine, Kinsta, etc.) set WP_ENVIRONMENT_TYPE=staging on clones automatically; WordPress defaults to production when unset, so ordinary single-environment sites are unaffected. Return true to opt a surface back in.
// QA environment that owns its own webhook endpoints — allow webhooks
// but keep IndexNow muted.
add_filter( 'perflocale/dispatch/allow_non_production', function ( bool $allow, string $feature ): bool {
return $feature === 'webhooks';
}, 10, 2 );Parameters:
bool $allow- Defaultfalse.string $feature- Dispatch surface:'webhooks'or'indexnow'.string $environment- Currentwp_get_environment_type()value.
File: src/Helper.php
perflocale/dispatch/blocked (action)
Fires when the non-production gate blocks a dispatch — the observability hook for loggers or admin notices that surface “webhooks are intentionally muted on this clone.”
add_action( 'perflocale/dispatch/blocked', static function ( string $feature, string $environment ): void {
error_log( "PerfLocale: {$feature} dispatch muted on {$environment} environment." );
}, 10, 2 );Parameters: string $feature, string $environment.
File: src/Helper.php
Garbage Collection & Retention
PerfLocale ships a layered GC system so every plugin-owned data store stays bounded. These filters tune the retention windows; the matching do_action hooks fire after each GC pass (see the Actions section below).
perflocale/strings/stale_retention_days
Number of days a perflocale_strings row can go un-rediscovered before the daily mark-and-sweep GC deletes it (and cascades to the matching perflocale_string_translations rows). Every StringRepository::bulk_insert() call (the scanner) and every register_setting_string() call touches the row's last_seen_at, so strings that are still in plugin / theme code or actively registered never expire. Strings whose source disappears (uninstalled plugin, removed theme, deleted code) age out automatically.
// Hold on to potentially-stale strings for a full year.
add_filter( 'perflocale/strings/stale_retention_days', fn() => 365 );
// Disable the strings GC entirely (rows accumulate until manually deleted).
add_filter( 'perflocale/strings/stale_retention_days', fn() => 0 );Parameters: int $days - Default 90. Set to 0 to disable the GC.
File: src/Database/Repository/StringRepository.php
perflocale/strings/manual_contexts
Allowlist of context values that the strings GC will never delete, even when last_seen_at exceeds the retention window. Use this for manually-registered strings whose register_setting_string() code path may not run regularly — workflow email templates, settings labels, etc.
// Preserve a custom-addon setting label that's only re-registered when the
// addon's admin page is rendered (might be longer than 90 days between hits).
add_filter( 'perflocale/strings/manual_contexts', function ( array $contexts ): array {
$contexts[] = 'my_addon_settings_label';
return $contexts;
} );Parameters: string[] $contexts - Default ['workflow_email_subject', 'workflow_email_body'].
File: src/Database/Repository/StringRepository.php
perflocale/tm/gc_row_cap
Maximum row count for the perflocale_translation_memory table. When the row count exceeds the cap, the weekly LRU eviction job deletes the lowest-scoring rows (ordered by usage_count ASC, updated_at ASC) down to perflocale/tm/gc_target_rows. Currently a no-op because TranslationMemory::store() has no production callers yet, but the cap is wired now so a future learn-on-save feature can't grow the table unbounded.
// High-traffic site with lots of distinct content — let the TM grow bigger.
add_filter( 'perflocale/tm/gc_row_cap', fn() => 500000 );Parameters: int $cap - Default 100000.
File: src/Translation/TranslationMemory.php
perflocale/tm/gc_target_rows
Target row count after the TM LRU GC fires. Must be less than perflocale/tm/gc_row_cap — the headroom between cap and target is how many writes the table accepts before the next GC tick. Misconfigured values (target ≥ cap, or target < 0) get sanity-clamped to 90 % of cap so a filter typo can't wipe the whole table.
// Pair with the 500K cap above — keep 450K rows after eviction.
add_filter( 'perflocale/tm/gc_target_rows', fn() => 450000 );Parameters: int $target - Default 90000.
File: src/Translation/TranslationMemory.php
JavaScript hooks (wp.hooks)
Beyond PHP, PerfLocale exposes one stable JavaScript filter via the WordPress @wordpress/hooks registry. Use it from a custom block plugin to teach the PerfLocale block-toolbar Translate action which attributes hold translatable text on your block type.
perflocale.blockToolbar.textAttrs (filter)
Filter the ordered list of attribute names the block toolbar will look at when extracting text from a block for machine translation. The default chain inspects the block’s registered attribute schema and returns names of string / rich-text attributes that look translatable (skipping className, anchor, alignment etc.). Return an extended array to opt your custom block in — cleanest extension surface that doesn’t require a code change in PerfLocale.
wp.hooks.addFilter(
'perflocale.blockToolbar.textAttrs',
'acme/my-block',
function ( chain, blockName ) {
if ( blockName === 'acme/quote-card' ) {
// Try `quoteHtml` first, then `attribution`, then fall back to defaults.
return [ 'quoteHtml', 'attribution' ].concat( chain );
}
return chain;
}
);Parameters:
string[] chain- Ordered list of attribute names. The toolbar walks this chain and translates the first attribute that returns a non-empty string.string blockName- The block being inspected (e.g.core/paragraph,acme/quote-card).
Return: array. An empty / non-array return value is ignored and the toolbar falls back to [ 'content' ].
File: assets/js/block-toolbar.js
Additional extension points
The following hooks cover more specialised extension points. They are stable and safe to use in production.
Addons & integrations
perflocale/addons/register
Fires after built-in addons are registered so third parties can register their own addon instances before boot.
add_action( 'perflocale/addons/register', function ( \PerfLocale\Addon\AddonRegistry $registry ): void {
$registry->register( new \My\Plugin\MyAddon() );
} );Parameters: \PerfLocale\Addon\AddonRegistry $registry.
File: src/Addon/AddonRegistry.php
perflocale/addons/registry
Filter the array of known addons as rendered on the Addons admin page.
add_filter( 'perflocale/addons/registry', function ( array $addons ): array {
unset( $addons['legacy-addon-id'] );
return $addons;
} );Parameters: array $addons - Map keyed by addon ID.
File: src/Admin/Pages/AddonsPage.php
perflocale/addon/before_migrate
Fires once per migration step, before the addon's migrate_to() runs.
add_action( 'perflocale/addon/before_migrate', function ( $addon, int $stored, int $target ): void {
error_log( sprintf( 'addon=%s stored=%d -> step=%d', $addon->get_id(), $stored, $target ) );
}, 10, 3 );Parameters: AddonInterface&HasSchema $addon, int $stored, int $target.
Args: 3
File: src/Addon/AddonSchemaManager.php
perflocale/addon/migrated
Fires after each successful addon migration step.
add_action( 'perflocale/addon/migrated', function ( $addon, int $version ): void {
// Warm caches, record telemetry, etc.
}, 10, 2 );Parameters: AddonInterface&HasSchema $addon, int $version.
Args: 2
File: src/Addon/AddonSchemaManager.php
perflocale/addon/migration_failed
Fires when an addon migration step throws. The addon is not quarantined by this filter - observe or re-raise as needed.
add_action( 'perflocale/addon/migration_failed', function ( $addon, int $version, \Throwable $e ): void {
error_log( sprintf( '[perflocale] %s v%d failed: %s', $addon->get_id(), $version, $e->getMessage() ) );
}, 10, 3 );Parameters: AddonInterface&HasSchema $addon, int $version, \Throwable $e.
Args: 3
File: src/Addon/AddonSchemaManager.php
perflocale/addon/manifest_written
Fires after an addon manifest is re-persisted to disk - used by the uninstall pipeline to know what to purge even when the addon is gone.
add_action( 'perflocale/addon/manifest_written', function ( string $addon_id, array $manifest ): void {
// Audit-log manifest changes.
}, 10, 2 );Parameters: string $addon_id, array $manifest.
Args: 2
File: src/Addon/AddonManifestWriter.php
perflocale/addon/before_uninstall
Fires before an addon's stored data is purged. Receives the computed PurgePlan snapshot.
add_action( 'perflocale/addon/before_uninstall', function ( string $addon_id, \PerfLocale\Addon\PurgePlan $plan ): void {
// Back up rows referenced by $plan before PerfLocale drops them.
}, 10, 2 );Parameters: string $addon_id, \PerfLocale\Addon\PurgePlan $plan.
Args: 2
File: src/Addon/AddonUninstaller.php
perflocale/addon/meta_purge_batch
Fires once per batched DELETE during addon uninstall meta cleanup.
add_action( 'perflocale/addon/meta_purge_batch', function ( string $type, int $deleted, int $total ): void {
// $type is 'postmeta' | 'termmeta' | 'usermeta' etc.
}, 10, 3 );Parameters: string $type, int $deleted, int $total.
Args: 3
File: src/Addon/AddonUninstaller.php
perflocale/addon/uninstalled
Fires after an addon's uninstall pipeline completes.
add_action( 'perflocale/addon/uninstalled', function ( string $addon_id, \PerfLocale\Addon\PurgeResult $result ): void {
// $result reports counts per table / meta type.
}, 10, 2 );Parameters: string $addon_id, \PerfLocale\Addon\PurgeResult $result.
Args: 2
File: src/Addon/AddonUninstaller.php
Addon settings UI
perflocale/settings/addon_subtabs
Register a Settings subtab belonging to your addon. Return a map of slug => label.
add_filter( 'perflocale/settings/addon_subtabs', function ( array $subtabs ): array {
$subtabs['my-addon'] = __( 'My Addon', 'my-plugin' );
return $subtabs;
} );Parameters: array $subtabs.
File: src/Admin/AdminController.php
perflocale/settings/render_addon_subtab
Render the form fields for a registered addon subtab.
add_action( 'perflocale/settings/render_addon_subtab', function ( string $subtab, \PerfLocale\Settings $settings ): void {
if ( $subtab !== 'my-addon' ) {
return;
}
// echo <tr> form-table rows here.
}, 10, 2 );Parameters: string $subtab, \PerfLocale\Settings $settings.
Args: 2
File: src/Admin/Pages/SettingsPage.php
perflocale/settings/addon_subtab_after
Fires after the addon subtab form-table, right before the submit button - use for secondary actions.
add_action( 'perflocale/settings/addon_subtab_after', function ( string $subtab, \PerfLocale\Settings $settings ): void {
if ( $subtab === 'my-addon' ) {
echo '<p class="description">' . esc_html__( 'Need help? See our docs.', 'my-plugin' ) . '</p>';
}
}, 10, 2 );Parameters: string $subtab, \PerfLocale\Settings $settings.
Args: 2
File: src/Admin/Pages/SettingsPage.php
perflocale/settings/extract_addon_values
Filter the sanitized settings being written for an addon subtab before they are merged into perflocale_settings.
add_filter( 'perflocale/settings/extract_addon_values', function ( array $values, string $tab ): array {
if ( $tab !== 'my-addon' ) {
return $values;
}
$values['my_key'] = sanitize_text_field( wp_unslash( $_POST['my_key'] ?? '' ) );
return $values;
}, 10, 2 );Parameters: array $values, string $tab.
Args: 2
File: src/Admin/Pages/SettingsPage.php
GeoIP
perflocale/geo/visitor_ip
Override the visitor IP used for GeoIP lookups. Useful for testing on localhost or for honouring a specific proxy header.
add_filter( 'perflocale/geo/visitor_ip', function ( string $ip ): string {
return $_SERVER['HTTP_X_REAL_IP'] ?? $ip;
} );Parameters: string $ip.
File: src/Router/GeoRedirect.php
perflocale/geo/lookup_country
Short-circuit the GeoIP lookup with your own data source (e.g. a Cloudflare CF-IPCountry header).
add_filter( 'perflocale/geo/lookup_country', function ( string $country, string $ip ): string {
if ( ! empty( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) {
return strtoupper( (string) $_SERVER['HTTP_CF_IPCOUNTRY'] );
}
return $country;
}, 10, 2 );Parameters: string $country (ISO-3166-1 alpha-2, or empty), string $ip.
Args: 2
File: src/Router/GeoRedirect.php
Routing & detection
perflocale/accept_language_limit
Maximum number of ranked entries parsed out of the Accept-Language HTTP header when matching the visitor's preferred language.
add_filter( 'perflocale/accept_language_limit', fn() => 5 );Parameters: int $limit - Default 20.
File: src/Router/LanguageRouter.php
perflocale/query/child_post_types
Post types treated as children of their parent (e.g. WooCommerce variations). Query filtering follows the parent's language to avoid split listings.
add_filter( 'perflocale/query/child_post_types', function ( array $types ): array {
$types[] = 'my_variant_cpt';
return $types;
} );Parameters: array $types - Default [ 'product_variation' ].
File: src/Translation/PostQueryFilter.php
Language switcher (menu placement)
perflocale/switcher/add_to_menu
Return true from within wp_nav_menu_items filtering to have the language switcher appended to a nav menu without an explicit menu location.
add_filter( 'perflocale/switcher/add_to_menu', function ( bool $add, \stdClass $args ): bool {
return $args->theme_location === 'primary';
}, 10, 2 );Parameters: bool $add, \stdClass $args.
Args: 2
File: src/Frontend/LanguageSwitcher.php
perflocale/switcher/menu_locations
Whitelist specific theme menu locations that should receive the language switcher.
add_filter( 'perflocale/switcher/menu_locations', function ( array $locations, \stdClass $args ): array {
return [ 'primary', 'header-utility' ];
}, 10, 2 );Parameters: array $locations, \stdClass $args.
Args: 2
File: src/Frontend/LanguageSwitcher.php
Machine translation
perflocale/mt/allowed_html
Filter the wp_kses-style allowed-tags map applied to translated HTML returned by MT providers. Tighten or loosen to taste.
add_filter( 'perflocale/mt/allowed_html', function ( array $allowed ): array {
$allowed['mark'] = [];
return $allowed;
} );Parameters: array $allowed.
File: src/MachineTranslation/TranslationService.php
Strings & content
perflocale/strings/regenerate_files
Fires after the cache invalidator decides a .mo regeneration is warranted. Hook in to trigger downstream compilation or to piggyback on the same cache-clear window.
add_action( 'perflocale/strings/regenerate_files', function ( \PerfLocale\Cache\CacheManager $cache ): void {
// Custom follow-up work.
} );Parameters: \PerfLocale\Cache\CacheManager $cache.
File: src/Cache/CacheInvalidator.php
perflocale/translation/dangerous_meta_patterns
Regex list for meta keys that must never be translated or synced (serialised PHP objects, private flags, etc.). Patterns are matched against the meta key.
add_filter( 'perflocale/translation/dangerous_meta_patterns', function ( array $patterns ): array {
$patterns[] = '/^_my_encrypted_/';
return $patterns;
} );Parameters: array $patterns.
File: src/Translation/PostTranslationManager.php
Cache & SEO
perflocale/cache/flush_archive_hreflang
Fires when the archive-page hreflang cache is invalidated for a post. Use it to purge an external CDN cache for the same URL group.
add_action( 'perflocale/cache/flush_archive_hreflang', function ( int $post_id ): void {
my_cdn_purge_archive_urls_for( $post_id );
} );Parameters: int $post_id.
File: src/Cache/CacheInvalidator.php
Admin & permissions
perflocale/abilities/enabled
Opt in to PerfLocale's WordPress Abilities API integration. The Abilities API ships in WordPress 6.9 and exposes plugin operations as discoverable, REST-callable, AI-tool-friendly abilities. PerfLocale registers six abilities in the perflocale-translation category, but they are off by default — they only register when this filter returns true. That keeps zero-overhead behaviour for installs that don't need them.
// Enable the integration. Required on WordPress 6.9+ for the abilities to appear.
add_filter( 'perflocale/abilities/enabled', '__return_true' );Once enabled, the following abilities register on the wp_abilities_api_init action and become accessible via wp_get_abilities(), the /wp/v2/abilities REST endpoint, and any tool that consumes the registry (Claude Desktop, Cursor, MCP-aware integrations):
perflocale/list-languages— return all configured languages.perflocale/get-translations— fetch the translation group for a post.perflocale/detect-language— detect the language of a piece of text.perflocale/convert-url— convert a URL between languages.perflocale/translate-post— machine-translate a post into a target language.perflocale/create-translation— create a new translation linked to an existing post.
On WordPress 6.8 and earlier the Abilities API isn't available; this filter has no effect because the registration callback never fires.
Parameters: bool $enabled - Default false.
File: src/Bootstrap.php
perflocale/block_toolbar/enabled
Toggle PerfLocale's block-editor toolbar button for flagging a block as non-translatable.
add_filter( 'perflocale/block_toolbar/enabled', '__return_false' );Parameters: bool $enabled - Default true.
File: src/Bootstrap.php
perflocale/menu/badge_post_limit
Upper bound on the number of posts scanned when computing the "pending translations" badge on the admin menu. Keeps badge counts cheap on very large sites.
add_filter( 'perflocale/menu/badge_post_limit', fn() => 10000 );Parameters: int $limit - Default 5000.
File: src/Translation/MenuManager.php
perflocale/migration/time_limit
Override the set_time_limit() value used during admin-triggered migration imports.
add_filter( 'perflocale/migration/time_limit', fn() => 900 );Parameters: int $seconds - Default 300.
File: src/Admin/AdminController.php
perflocale/admin/bulk_time_limit
Override the set_time_limit() value used during the two bulk admin AJAX handlers: Create WooCommerce page translations and Create taxonomy translations. Both are gated by check_ajax_referer() + current_user_can('manage_options'), and the time-limit raise happens only AFTER those gates pass. Default 0 (no limit). Set a positive int (seconds) if your host enforces a hard cap and you'd rather error out cleanly than have the handler abort mid-batch.
// Cap the bulk admin handlers at 10 minutes on this host.
add_filter( 'perflocale/admin/bulk_time_limit', fn(): int => 600 );Parameters: int $seconds - Default 0 (no limit).
File: src/Bootstrap.php
Webhooks
perflocale/webhooks/url_safe
Final gate before a webhook URL is dispatched - return false to reject. The built-in checks already block private/loopback/metadata IPs; use this filter to layer org-specific policy on top.
add_filter( 'perflocale/webhooks/url_safe', function ( bool $safe, string $url ): bool {
$host = wp_parse_url( $url, PHP_URL_HOST );
return $safe && in_array( $host, [ 'hooks.my-org.example' ], true );
}, 10, 2 );Parameters: bool $safe, string $url.
Args: 2
File: src/Api/WebhookController.php