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
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
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
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
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
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
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
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
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).
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/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 7.0+ Integrations (feature-detected — inert on earlier WordPress versions)
Hooks that wire PerfLocale into APIs shipping with WordPress 7.0 — the AI Client, the Connectors API, the JS-side Abilities API, and Block Hooks. Each surface is feature-detected; on a site running WordPress 6.x these filters fire but the 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/glossary/suggest_prompt
Override the prompt used by the Glossary admin form's suggest-as-you-type field (only invoked when the active MT provider is wp_ai_client). Keep the "return only the translated term" instruction or the cleanup pass will need to strip extra noise.
add_filter( 'perflocale/glossary/suggest_prompt', function ( string $prompt, string $term, string $source, string $target, array $prior ): string {
return $prompt . "\n\nFollow our internal style guide: prefer formal register, never use anglicisms.";
}, 10, 5 );Parameters: string $prompt, string $term, string $source_slug, string $target_slug, array $prior_examples.
File: src/Api/GlossaryScanController.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
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
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 the automatic GeoIP + browser-language redirects. Return false to suppress both until the visitor has granted consent. Default true - redirects fire immediately, matching the pre-consent-framework behaviour.
// 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
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/fse/translatable_blocks
Filter which block types are translatable in the Full Site Editor.
Parameters: array $blocks - Array of block type names.
File: src/Admin/SiteEditorIntegration.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: 100 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/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/batch_size
Rows committed per chunked-import REST call (POST /perflocale/v1/import/batch). Default 100. Higher values cut round-trip count but lengthen the per-batch transaction window and PHP memory peak. Clamped to 10–2000. The second arg is the table name so callers can pick different sizes per table.
add_filter( 'perflocale/import/batch_size', static function ( int $size, string $table ): int {
return $table === 'strings' ? 500 : 100; // bigger batches for the big table
}, 10, 2 );Parameters: int $size, string $table.
File: src/Api/ImportController.php
perflocale/import/max_file_bytes
Maximum size of the JSON envelope accepted by POST /perflocale/v1/import/upload. 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/Api/ImportController.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/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/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/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
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/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
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
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/switched
Fires when the language is switched programmatically via switch_language().
Parameters:
string $slug- New language slug.string $previous- Previous language slug.
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/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
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
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_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/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
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/addons/suppress_hreflang
Let an SEO-plugin addon suppress PerfLocale's own hreflang output when it already emits the tags. Returns a boolean.
add_filter( 'perflocale/addons/suppress_hreflang', function ( bool $suppress ): bool {
return true; // Let the host SEO plugin own hreflang entirely.
} );Parameters: bool $suppress - Default false.
File: src/Addon/HreflangCoordinator.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