Developer API
This document covers how third-party plugins can extend PerfLocale without modifying its source files. All extension points use standard WordPress filters and actions.
Building an addon that owns its own database tables? See the dedicated Addon System page for the HasSchema, HasUninstallTargets, and HasCustomUninstall capability interfaces - they give your addon managed migrations, a manifest-driven uninstall, and strict namespace isolation, all with four worked examples covering schema, meta + cron, custom cleanup callbacks, and multisite.
Registering an External Addon
External plugins can register as first-class PerfLocale addons without placing files in the addons/ directory.
Step 1: Implement AddonInterface
<?php
// my-perflocale-addon/my-perflocale-addon.php
use PerfLocale\Addon\AddonInterface;
use PerfLocale\Plugin;
class MyPerfLocaleAddon implements AddonInterface {
public function get_id(): string {
return 'my-addon';
}
public function get_name(): string {
return 'My Addon';
}
public function get_version(): string {
return '1.0.0';
}
public function get_required_plugins(): array {
// Return empty array if no dependencies.
return [];
}
public function is_compatible(): bool {
// Return true if this addon should activate.
return true;
}
public function boot( Plugin $plugin ): void {
// Register your hooks here. $plugin provides access to:
// $plugin->get( 'settings' ) - Settings instance
// $plugin->get( 'router' ) - Language router
// $plugin->get( 'cache' ) - Cache manager
}
public function get_settings_fields(): array {
return [];
}
}Step 2: Register on the perflocale/addons/register action
add_action( 'perflocale/addons/register', function ( $registry ) {
$registry->register( new MyPerfLocaleAddon() );
} );Timing: This action fires at plugins_loaded priority 20, after all plugins are active. Your plugin's main file runs before this, so the hook is always available.
Safety: The addon's boot() runs inside a try/catch - a crash in your addon won't take down the site (errors are logged when WP_DEBUG is enabled).
Addons page: Registered addons automatically appear on the Addons page. To control the card (category, icon, description, settings link), add a get_card_info() method to your addon class:
public function get_card_info(): array {
return [
'description' => __( 'Does amazing things.', 'my-textdomain' ),
'category' => 'seo', // feature, theme, seo, ecommerce, builder, fields, forms
'icon' => 'dashicons-star-filled',
'requires' => __( 'My Plugin v1.0', 'my-textdomain' ),
'settings_tab' => 'my-addon', // Slug - links to Settings > Addons > my-addon
];
}All keys are optional - unset keys use sensible defaults (category defaults to feature, icon to dashicons-admin-plugins).
Available categories: feature, theme, seo, ecommerce, builder, fields, forms.
Alternatively, use the perflocale/addons/registry filter for full control without implementing the interface:
add_filter( 'perflocale/addons/registry', function ( array $addons ): array {
$addons['my-addon'] = [
'name' => 'My Addon',
'description' => __( 'Does amazing things.', 'my-textdomain' ),
'category' => 'seo',
'icon' => 'dashicons-star-filled',
'requires' => __( 'My Plugin v1.0', 'my-textdomain' ),
'settings_tab' => 'my-addon',
'check' => fn() => true,
];
return $addons;
} );Adding a Settings Subtab
Add a settings page under Settings > Addons with your own form fields.
Step 1: Register the subtab
add_filter( 'perflocale/settings/addon_subtabs', function ( array $subtabs ): array {
$subtabs['my-addon'] = __( 'My Addon', 'my-textdomain' );
return $subtabs;
} );Step 2: Render your settings fields
add_action( 'perflocale/settings/render_addon_subtab', function ( string $subtab, $settings ) {
if ( $subtab !== 'my-addon' ) {
return;
}
?>
<tr>
<th scope="row"><?php esc_html_e( 'API Key', 'my-textdomain' ); ?></th>
<td>
<input type="text" name="my_addon_api_key"
value="<?php echo esc_attr( $settings->get( 'my_addon_api_key', '' ) ); ?>">
</td>
</tr>
<?php
}, 10, 2 );Step 3: Handle saving
add_filter( 'perflocale/settings/extract_addon_values', function ( array $values, string $tab ): array {
if ( $tab !== 'my-addon' ) {
return $values;
}
return [
'my_addon_api_key' => isset( $_POST['my_addon_api_key'] )
? sanitize_text_field( wp_unslash( $_POST['my_addon_api_key'] ) )
: '',
];
}, 10, 2 );Important: Your setting keys must be registered in PerfLocale's Settings::DEFAULTS array, or use WordPress update_option() directly for custom storage.
Custom Exchange Rate Provider
Add a custom exchange rate data source for multi-currency.
add_filter( 'perflocale/woocommerce/exchange_rate_providers', function ( array $providers ): array {
$providers['my_rates_api'] = [
'name' => 'My Rates API',
'needs_key' => true,
'key_setting' => 'wc_my_rates_key',
'fetch_callback' => function ( string $base, array $targets, string $api_key ): array {
$response = wp_remote_get( 'https://api.example.com/rates?' . http_build_query( [
'base' => $base,
'symbols' => implode( ',', $targets ),
'key' => $api_key,
] ) );
if ( is_wp_error( $response ) ) {
return [];
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
// Return: [ 'GBP' => 0.87, 'BGN' => 1.96 ]
return $data['rates'] ?? [];
},
];
return $providers;
} );Related hooks
| Hook | Type | Description |
|---|---|---|
perflocale/woocommerce/exchange_rates_fetched | Filter | Modify rates before saving |
perflocale/woocommerce/exchange_rates_synced | Action | Fires after rates are saved |
Custom GeoIP Provider
Add a custom geolocation service for visitor country detection.
add_filter( 'perflocale/geo/providers', function ( array $providers ): array {
$providers['my_geoip'] = [
'name' => 'My GeoIP Service',
'needs_key' => true,
'key_setting' => 'geo_my_geoip_key',
'fetch_callback' => function ( string $ip, $settings ): string {
$response = wp_remote_get( "https://geo.example.com/{$ip}" );
if ( is_wp_error( $response ) ) {
return '';
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
return $data['country_code'] ?? '';
},
];
return $providers;
} );Related hooks
| Hook | Type | Description |
|---|---|---|
perflocale/geo/visitor_ip | Filter | Override IP detection (useful for proxies/CDNs) |
perflocale/geo/country_code | Filter | Modify detected country code |
perflocale/geo/country_map | Filter | Modify country-to-language mapping |
perflocale/geo/redirect_language | Filter | Override redirect language selection |
perflocale/geo/lookup_country | Filter | Bypass providers entirely with custom lookup |
Custom Machine Translation Provider
Extend AbstractProvider to add a custom MT engine (e.g., a self-hosted model).
use PerfLocale\MachineTranslation\AbstractProvider;
class MyMtProvider extends AbstractProvider {
public function get_id(): string { return 'my_mt'; }
public function get_name(): string { return 'My Translation API'; }
public function is_configured(): bool {
return (string) $this->settings->get( 'mt_my_api_key' ) !== '';
}
/**
* @param bool $fast_fail When true, use a short timeout - called from
* synchronous REST contexts (block editor, REST API).
*/
public function translate( string $text, string $source_lang, string $target_lang, bool $fast_fail = false ): string {
$response = $this->make_request(
'https://mt.example.com/translate',
[
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer ' . $this->settings->get( 'mt_my_api_key' ),
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [ 'text' => $text, 'target' => $target_lang ] ),
'timeout' => 30,
],
3, // retries
$fast_fail // pass through - make_request() enforces 1 retry + 8 s cap
);
$data = $this->parse_json_response( $response['body'] );
$this->track_usage( $text );
return (string) ( $data['translation'] ?? $text );
}
public function test_connection(): bool {
$this->translate( 'test', 'en', 'es' );
return true;
}
}
add_filter( 'perflocale/machine_translation/providers', function ( array $providers ): array {
$providers['my_mt'] = new MyMtProvider( $settings );
return $providers;
} );Important: implement ProviderInterface (or extend AbstractProvider) and always accept and pass through the $fast_fail parameter in both translate() and translate_batch(). When $fast_fail = true the calling context is a synchronous REST request (block editor, machine-translate REST endpoint) and the user is waiting - your provider should fail fast rather than retrying.
Related hooks
| Hook | Type | Description |
|---|---|---|
perflocale/machine_translation/text_before_send | Filter | Modify text before sending to provider |
perflocale/machine_translation/result | Filter | Modify translated result |
perflocale/machine_translation/before | Action | Fires before translation begins |
perflocale/machine_translation/after | Action | Fires after translation succeeds |
perflocale/machine_translation/failed | Action | Fires on translation failure |
Theme Integration
Themes can register a language switcher element or declare compatibility.
Adding translatable content types
// Register a custom post type as translatable.
add_filter( 'perflocale/translatable_post_types', function ( array $types ): array {
$types[] = 'my_custom_post_type';
return $types;
} );
// Register a custom taxonomy as translatable.
add_filter( 'perflocale/translatable_taxonomies', function ( array $taxes ): array {
$taxes[] = 'my_custom_taxonomy';
return $taxes;
} );Customizing the language switcher
// Filter languages shown in the switcher.
add_filter( 'perflocale/switcher/languages', function ( array $languages ): array {
// Remove a language from the switcher.
unset( $languages['ar'] );
return $languages;
} );
// Filter the final switcher HTML.
add_filter( 'perflocale/switcher/output', function ( string $html ): string {
return '<div class="my-wrapper">' . $html . '</div>';
} );Securing API Keys
PerfLocale resolves every API key (DeepL, Google Translate, Fixer.io, GeoIP providers, etc.) from three sources in priority order:
- Environment variable — e.g.
PERFLOCALE_DEEPL_API_KEY. Read viagetenv(). Highest priority. Recommended for containerised, CI, or managed-host deployments. - PHP constant in
wp-config.php— same canonical name (define( 'PERFLOCALE_DEEPL_API_KEY', '…' )). Recommended when env vars aren’t available. - Admin settings (database) — lowest priority, used when neither override is set.
The first non-empty source wins. When an env var or constant is active, the matching admin field is disabled and shows where the value comes from; the value is never written to the database or included in PerfLocale’s data exports.
Supported canonical names
// Each name is used identically as both env-var name and PHP-constant name.
// Machine translation
PERFLOCALE_DEEPL_API_KEY
PERFLOCALE_GOOGLE_API_KEY
PERFLOCALE_MICROSOFT_API_KEY
PERFLOCALE_LIBRE_API_KEY
PERFLOCALE_LIBRE_URL
PERFLOCALE_AGENCY_URL
PERFLOCALE_AGENCY_API_KEY
// GeoIP providers
PERFLOCALE_IPINFO_TOKEN
PERFLOCALE_IPINFO_LITE_TOKEN
PERFLOCALE_IPSTACK_KEY
PERFLOCALE_IP_API_KEY
// Exchange-rate providers
PERFLOCALE_OXR_API_KEY
PERFLOCALE_CURRENCYFREAKS_KEY
PERFLOCALE_FIXER_KEYSee the dedicated API Keys: Environment Variables & Constants reference for examples and the full priority semantics.
Why this matters
- Database backups won’t expose API keys
- Staging/dev environments can use different keys via per-environment env vars or
wp-config.phpincludes - wp-admin access doesn’t reveal keys (the settings field is disabled and shows the source)
- Version control — both env vars and
wp-config.phpcan be excluded from repos
Hooks Reference
Addon Lifecycle
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/addons/register | Action | AddonRegistry $registry | Register external addons |
perflocale/addons/registered | Filter | array $addons | Modify addons before boot |
perflocale/addon/is_compatible | Filter | bool $compatible, string $id | Override compatibility |
perflocale/addon/activated | Action | string $id | After addon boots |
perflocale/addons/loaded | Action | - | After all addons boot |
Settings
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/settings/addon_subtabs | Filter | array $subtabs | Add settings subtabs |
perflocale/settings/render_addon_subtab | Action | string $subtab, Settings $settings | Render subtab UI |
perflocale/settings/addon_subtab_after | Action | string $subtab, Settings $settings | After form-table |
perflocale/settings/extract_addon_values | Filter | array $values, string $tab | Provide save values |
perflocale/settings/updated | Action | array $new, array $old | After settings saved |
Language & Routing
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/language/detected | Action | string $slug, string $method | After language detected |
perflocale/language/switched | Action | string $slug | After programmatic switch |
perflocale/active_languages | Filter | array $languages | Filter active languages |
perflocale/excluded_paths | Filter | array $paths | URL paths to skip |
perflocale/url/convert | Filter | string $url | Filter converted URLs |
Content & Translation
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/translatable_post_types | Filter | array $types | Translatable post types |
perflocale/translatable_taxonomies | Filter | array $taxes | Translatable taxonomies |
perflocale/translation/created | Action | int $id, string $type, string $lang | Translation created |
perflocale/sync_fields | Filter | array $fields | Fields to sync across translations |
Frontend
Summary of every switcher hook. See the Hooks reference for full parameter signatures and examples, and the Language Switcher page for the matching JS events.
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/switcher/languages | Filter | array $languages | Language list shown in the switcher |
perflocale/switcher/link_attrs | Filter | array $attrs, object $lang, string $current_slug, array $args | HTML attributes on each per-language link (data-*, rel, hreflang override, lang/dir override, …) |
perflocale/switcher/option_content | Filter | string $inner, object $lang, string $current_slug, array $attrs | Inner HTML of each option (flag span + label span), fires from every render surface |
perflocale/switcher/panel_before | Filter | string $html, array $languages, string $current_slug, array $attrs | HTML injected at the start of the dropdown panel (inside the listbox) |
perflocale/switcher/panel_after | Filter | string $html, array $languages, string $current_slug, array $attrs | HTML injected at the end of the dropdown panel |
perflocale/switcher/arrow_html | Filter | string $html, string $style | Override the dropdown trigger's chevron icon |
perflocale/switcher/sanitize_output | Filter | bool $sanitize, string $html | Toggle whether kses_switcher() runs wp_kses (default true) |
perflocale/switcher/kses_allowed_html | Filter | array $allowed, string $html | Extend the kses_switcher() allowlist with extra tags / attributes |
perflocale/switcher/output | Filter | string $html, array $args | Last-resort hook on the fully-rendered switcher HTML |
perflocale/switcher/add_to_menu | Filter | bool $add, \stdClass $args | Auto-inject the switcher into a registered theme menu location |
perflocale/switcher/menu_locations | Filter | array $locations, \stdClass $args | Restrict menu auto-injection to specific theme menu locations |
perflocale/seo/hreflang_tags | Filter | array $tags | Hreflang tags emitted in the HTML head + HTTP Link header |
Three bubbling CustomEvents also fire on the switcher's <nav> root for JS integrations: perflocale:switcher:open and perflocale:switcher:close (fired after the panel state changes; not cancelable), and perflocale:switcher:navigate (fired BEFORE the browser follows a clicked option's URL; cancelable via event.preventDefault() for "unsaved changes" guards, pre-switch analytics with navigator.sendBeacon, custom redirect middleware, A/B testing tools). See JavaScript events for detail-object shapes + usage examples.
WooCommerce
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/woocommerce/exchange_rate_providers | Filter | array $providers | Rate providers |
perflocale/woocommerce/exchange_rates_fetched | Filter | array $rates, string $base, string $provider | Before save |
perflocale/woocommerce/exchange_rates_synced | Action | array $rates, string $base, string $provider | After save |
GeoIP
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/geo/providers | Filter | array $providers | GeoIP providers |
perflocale/geo/visitor_ip | Filter | string $ip | Override IP |
perflocale/geo/country_code | Filter | string $code, string $ip | Override country |
perflocale/geo/country_map | Filter | array $map | Country-to-language map |
perflocale/geo/redirect_language | Filter | string $slug, string $country | Override redirect |
Machine Translation
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/machine_translation/providers | Filter | array $providers | MT providers |
perflocale/machine_translation/text_before_send | Filter | string $text | Pre-send filter |
perflocale/machine_translation/result | Filter | string $result | Post-translate filter |
Plugin Lifecycle
| Hook | Type | Description |
|---|---|---|
perflocale/loaded | Action | Plugin fully booted |
perflocale/activated | Action | Plugin activated |
perflocale/deactivated | Action | Plugin deactivated |
perflocale/cache/flush_all | Action | All caches flushed |
WordPress Abilities API
PerfLocale integrates with the WordPress Abilities API (introduced in WordPress 6.9). This makes PerfLocale's core translation operations discoverable and executable by AI tools, external consumers, and the standardized Abilities REST API.
The integration is disabled by default. Enable it with a filter:
add_filter( 'perflocale/abilities/enabled', '__return_true' );When enabled, PerfLocale registers these abilities:
| Ability | Type | Permission | Description |
|---|---|---|---|
perflocale/list-languages | Read | Public | List all active languages |
perflocale/get-translations | Read | perflocale_translate | Get all language versions of a post or term |
perflocale/detect-language | Read | perflocale_translate | Detect what language a post or term is in |
perflocale/convert-url | Read | Public | Convert a URL to a different language |
perflocale/translate-post | Mutation | perflocale_use_mt | Machine-translate a post |
perflocale/create-translation | Mutation | perflocale_translate | Create a translation stub |
Each ability includes JSON Schema input/output validation, proper permission callbacks, and MCP annotations for AI agent integration. On WordPress versions below 6.9, this feature has zero overhead - the hooks simply never fire.