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 stores API keys (DeepL, Google Translate, Fixer.io, etc.) in wp_options. For production environments, define keys as PHP constants in wp-config.php instead - PerfLocale checks constants first and skips database storage when they're defined.
Supported constants
// wp-config.php
// Machine Translation providers
define( 'PERFLOCALE_MT_DEEPL_API_KEY', 'your-deepl-key' );
define( 'PERFLOCALE_MT_GOOGLE_API_KEY', 'your-google-key' );
define( 'PERFLOCALE_MT_MICROSOFT_API_KEY', 'your-microsoft-key' );
define( 'PERFLOCALE_MT_LIBRE_API_KEY', 'your-libre-key' );
define( 'PERFLOCALE_MT_LIBRE_URL', 'https://your-libre-instance.com' );
// Exchange rate providers
define( 'PERFLOCALE_WC_OXR_API_KEY', 'your-openexchangerates-key' );
define( 'PERFLOCALE_WC_CURRENCYFREAKS_KEY', 'your-currencyfreaks-key' );
define( 'PERFLOCALE_WC_FIXER_KEY', 'your-fixer-key' );
// GeoIP providers
define( 'PERFLOCALE_GEO_IPINFO_KEY', 'your-ipinfo-key' );
define( 'PERFLOCALE_GEO_IPSTACK_KEY', 'your-ipstack-key' );
Why this matters
- Database backups won't expose API keys
- Staging/dev environments can use different keys via environment-specific
wp-config.php - wp-admin access doesn't reveal keys (the settings field shows "Defined in wp-config.php")
- Version control -
wp-config.phpis typically excluded from repos
When a constant is defined, the corresponding settings field becomes read-only and the value is never written to the database.
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
| Hook | Type | Args | Description |
|---|---|---|---|
perflocale/switcher/languages | Filter | array $languages | Switcher languages |
perflocale/switcher/output | Filter | string $html | Switcher HTML |
perflocale/switcher/add_to_menu | Filter | bool $add | Add switcher to menu |
perflocale/seo/hreflang_tags | Filter | array $tags | Hreflang tags |
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.