Helper API
PerfLocale provides a fluent helper API via the perflocale() function. This is the recommended way for theme and plugin developers to interact with PerfLocale.
Quick Start
// Get the helper.
$pl = perflocale();
// Current language info.
echo $pl->slug(); // "fr"
echo $pl->locale(); // "fr_FR"
echo $pl->name(); // "French"
echo $pl->native_name(); // "Français"
echo $pl->display_name(); // "French (Français)"
echo $pl->flag(); // "fr" (the stored flag code)
echo $pl->flag_emoji(); // "🇫🇷"Wherever a method or template tag below takes a language, it accepts the language’s slug ('de'), its locale ('de_DE') or a BCP 47 tag ('de-DE'), in any case. A value that names no active language raises a _doing_it_wrong() notice (shown with WP_DEBUG on). The formatting and date methods also take a well-formed locale that no language on the site uses.
Method Reference
Language Information
perflocale()->slug(): string
Returns the current language slug (e.g. "fr"). Empty string if no language detected.
perflocale()->locale(): string
Returns the full locale (e.g. "fr_FR"). Falls back to get_locale().
perflocale()->name(): string
Returns the English name of the current language (e.g. "French").
perflocale()->native_name(): string
Returns the native name (e.g. "Français"). Falls back to the English name.
perflocale()->display_name(): string
Returns a combined display string: "French (Français)". If English and native names are the same, returns just the name.
perflocale()->flag(): string
Returns the current language’s stored flag code (e.g. "fr"), or an empty string when none is set. For the emoji, use flag_emoji().
perflocale()->flag_emoji( ?string $lang = null ): string
Returns a language’s flag emoji, as the switcher shows it (e.g. "🇫🇷"). $lang is a slug, locale or BCP 47 tag; null means the current language. Without a stored flag code, the country of the locale is used (fr_FR gives 🇫🇷), then a two-letter slug. Returns an empty string when no language on the site has that slug or locale.
echo perflocale()->flag_emoji(); // current language
echo perflocale()->flag_emoji( 'pt-BR' ); // "🇧🇷" when the site has Portuguese (Brazil)perflocale()->current_language(): ?object
Returns the full language object with all properties, or null if not detected.
Language object properties:
| Property | Type | Example |
|---|---|---|
slug | string | "fr" |
locale | string | "fr_FR" |
name | string | "French" |
native_name | string | "Français" |
flag | string | "fr" |
text_direction | string | "ltr" |
is_default | string | 0 |
is_active | string | 1 |
Language Checks
perflocale()->is_default(): bool
Returns true if the current language is the default language.
if ( ! perflocale()->is_default() ) {
// Show "back to English" link.
}perflocale()->is_rtl(): bool
Returns true if the current language is right-to-left (Arabic, Hebrew, etc.). Without a current language, it returns WordPress’s own is_rtl().
$dir = perflocale()->is_rtl() ? 'rtl' : 'ltr';
echo '<div dir="' . $dir . '">';perflocale()->with_language( $language, callable $callback )
Run a callback with a different language imposed, then restore the previous one. This is the general primitive for rendering part of a request in another language from PHP - a custom template, a page-builder code block, a preview, a notification assembled outside its recipient’s request. It composes with anything rather than needing a separate helper per block type.
echo perflocale()->with_language( 'de', fn() => do_blocks( $markup ) );
echo perflocale()->with_language( 'de', fn() => do_shortcode( '[my_widget]' ) );
$de_title = perflocale()->with_language( 'de', fn() => get_bloginfo( 'name' ) );$language accepts a slug ('de'), a locale ('de_DE') or a BCP 47 tag ('de-DE') in any case, a post ID or WP_Post (uses that post’s language), or a language object. An unknown language raises a _doing_it_wrong() notice and the callback runs in the current language. Nesting is safe - each call restores its own predecessor - and the language is restored even if the callback throws.
Inside the window, string translations, the site title and tagline, synced-pattern and navigation references, and this helper’s own accessors (slug(), locale(), is_rtl()) all follow the imposed language.
Two limits worth knowing. The block editor and other admin contexts never rewrite pattern or navigation references at any language - that guard prevents a rewritten reference being saved over the source. And block-template content (wp_template / wp_template_part) is translated only when Full Site Editing is enabled; a window follows it when it is, and cannot conjure it when it is not.
Cost. Each actual language change rebuilds the per-language string map. That is fine around a template or a handful of blocks, and wrong inside a loop over posts or a filter that fires hundreds of times per request. To translate a single string, use translate() below, which touches no request state.
perflocale()->translate( string $text, $language = '', array $args = [] ): string
Translate one registered string, optionally into a named language. Unlike with_language() this changes no request state - it is two indexed lookups, so it is safe in a loop and safe to call for several languages in a row.
perflocale()->translate( 'Read more' ); // current language
perflocale()->translate( 'Read more', 'de' ); // by slug
perflocale()->translate( 'Read more', 'de_DE' ); // by locale
perflocale()->translate( 'Read more', 'de-DE' ); // by BCP 47 tag
perflocale()->translate( 'Read more', $post ); // that post's language
perflocale()->translate( 'Read more', 'de', [
'domain' => 'my-theme',
'context' => 'button',
'default' => 'Read more',
] );It only translates strings PerfLocale already knows about. A string is identified by the triple domain|context|text and must already exist on the Strings screen - put there by a scan, or registered by code. An unregistered string is returned unchanged, for ever, with no error. That is the most likely reason this function appears “not to work”: register the string first, then translate it.
An unknown language raises a _doing_it_wrong() notice and the current language is used.
The domain matters as much as the text. It defaults to PerfLocale’s own perflocale domain, so a theme string scanned under my-theme will not be found unless you pass its domain.
perflocale()->is_language( string $slug, bool $match_base = false ): bool
Whether the current language is the one a slug, locale or BCP 47 tag names. The match is exact by default: 'fr', 'fr_FR' and 'fr-FR' all match a French language with the locale fr_FR, and none of them matches fr_CA. With $match_base set to true, a value with the same base language matches too: 'fr' then also matches fr_CA.
if ( perflocale()->is_language( 'fr' ) ) {
echo 'Bienvenue!';
} elseif ( perflocale()->is_language( 'de' ) ) {
echo 'Willkommen!';
}
// Every French variant: fr_FR, fr_CA, fr_BE ...
if ( perflocale()->is_language( 'fr', true ) ) {
get_template_part( 'partials/legal', 'fr' );
}Language Data
perflocale()->default_language(): ?object
Returns the default language object.
$default = perflocale()->default_language();
echo $default->name; // "English"perflocale()->languages(): array
Returns all active languages as an array of objects.
foreach ( perflocale()->languages() as $lang ) {
echo $lang->slug . ': ' . $lang->native_name . "\n";
}
// en: English
// fr: Français
// de: DeutschURLs
perflocale()->permalink( int|WP_Post|null $post_id, string $lang_slug ): string
Get the translated permalink for a post in a specific language. $post_id takes what get_permalink() takes: a post ID, a WP_Post, or null for the current post. $lang_slug takes a slug, locale or BCP 47 tag.
$de_url = perflocale()->permalink( 42, 'de' );
// https://example.com/de/about/perflocale()->term_link( int|WP_Term $term_id, string $lang_slug ): string
Get the translated term archive link. $term_id takes a term ID or a WP_Term; $lang_slug a slug, locale or BCP 47 tag.
$de_cat = perflocale()->term_link( 5, 'de' );
// https://example.com/de/category/news/perflocale()->home_url( string $lang_slug = '', string $path = '' ): string
Get the home URL for a specific language, named by its slug, locale or BCP 47 tag.
perflocale()->home_url( 'de' );
// https://example.com/de/
perflocale()->home_url( 'de', '/contact/' );
// https://example.com/de/contact/
perflocale()->home_url();
// Current language home URL.Translations
perflocale()->translations( int $post_id, bool $public_only = false ): array
Get all translations of a post as a [slug => post_id] map. The map includes draft, pending and private translations. With $public_only = true it holds only what every visitor can see: published translations of a post type visitors can view. Use that whenever the result is shown on the front end.
$translations = perflocale()->translations( 42 );
// ['en' => 42, 'fr' => 56, 'de' => 78]
$published = perflocale()->translations( 42, true );
// ['en' => 42, 'fr' => 56] while the German translation is a draft
foreach ( $translations as $slug => $id ) {
echo $slug . ': ' . get_the_title( $id ) . "\n";
}perflocale()->translations_many( array $post_ids, bool $public_only = false ): array
Batch version. Resolves translations for many posts in two queries total (one to map ids to translation groups, one to fetch the group rows) regardless of input size - same data shape as translations() per source id. Use this on archive / loop templates instead of looping translations() per item. $public_only works as in translations() and reads the translations’ posts in one more query.
$post_ids = wp_list_pluck( $query->posts, 'ID' );
$siblings = perflocale()->translations_many( $post_ids, true );
foreach ( $query->posts as $post ) {
$map = $siblings[ $post->ID ]; // always an array, never missing
foreach ( $map as $slug => $translation_id ) {
echo $slug . ': ' . get_the_title( $translation_id ) . "\n";
}
}Posts with no translations get an empty array for that key (never a missing key), so callers don't need isset() guards. Invalid ids (0, negatives, deleted posts) get the same empty-array treatment.
perflocale()->is_translatable( string $name, string $type = 'post_type' ): bool
Shortcut for "is this post type / taxonomy enabled for translation in Settings → Translation?" Saves callers from reaching into the Settings service directly.
if ( perflocale()->is_translatable( 'product' ) ) {
// Render a "translate this product" CTA.
}
if ( perflocale()->is_translatable( 'product_cat', 'taxonomy' ) ) {
// Render translatable-term controls.
}Returns false for empty names and unknown types, and (defensively) when the settings service isn't reachable - same conservative default the rest of the Helper API uses.
Addons
perflocale()->is_addon_active( string $addon_id ): bool
Whether an addon booted in this request. Use it before code that relies on an integration’s hooks being in place, rather than checking whether the host plugin is installed.
if ( perflocale()->is_addon_active( 'woocommerce' ) ) {
// PerfLocale's WooCommerce integration is running in this request.
}Returns false before the addon registry has booted (init priority 0 on a normal request), for an empty ID, and for an addon that is not installed, is switched off on the Addons page, is quarantined or incompatible, or was turned off for this request by the perflocale/addon/enabled filter. The answer is for the current request only: the same addon can be active in one context and not in another.
perflocale()->addon( string $id ): ?object
The PHP API object of a booted addon, created on the first call and kept for the request. An addon offers one through HasPublicApi. Returns null for an empty or unknown ID, an addon that did not boot (the same rule as is_addon_active()), an addon without an API, and an API that could not be created. The object has no magic methods, so check a method with method_exists() before you call it.
$api = perflocale()->addon( 'my-addon' );
if ( $api && method_exists( $api, 'sync_now' ) ) {
$api->sync_now();
}perflocale()->woocommerce(): PerfLocale\WooCommerce\Api
The WooCommerce add-on’s PHP API, the same object as addon( 'woocommerce' ) once the add-on booted. It is never null, so a call chain cannot end in a fatal error: while the add-on is not active, its methods return a WP_Error. Like WooCommerce’s own stock functions, it makes no capability check.
stock_drift( array $args = [] ): array|WP_Error- the copy groups whose stock differs; it only reads.product_ids: only the groups of these products (1 to 500, any language, not variations);limit: stop after this many groups (0: all).align_stock( array $args = [] ): array|WP_Error- not available in this version: it returns theWP_Errorperflocale_stock_align_off(“Aligning stock is not available in this version.”).
Locale-aware formatting
perflocale()->format_number( $value, ?string $lang_slug = null, ?int $decimals = null ): string
Format a number using the current (or specified) language's conventions. $lang_slug takes a language slug, or a locale or BCP 47 tag in any case (de_DE, de-AT); a well-formed locale formats even when no language on the site uses it. An unknown value raises a _doing_it_wrong() notice and the current language is used. Uses PHP's NumberFormatter when the intl extension is loaded and ICU accepts the language's locale; otherwise it falls back to number_format_i18n(). With no $decimals override the fallback keeps the value's decimal places, to a maximum of three - the same default NumberFormatter applies - so both paths return the same number.
perflocale()->format_number( 1234.56 ); // "1,234.56" in en-US
perflocale()->format_number( 1234.56, 'de' ); // "1.234,56" in de-DE
perflocale()->format_number( 1234.56, 'de-AT' ); // "1 234,56" (U+00A0 between the groups)
perflocale()->format_number( 1234.56, 'fr_FR' ); // "1 234,56" (U+202F between the groups)
perflocale()->format_number( 1234.5, null, 0 ); // "1,235" (0 decimals)No-break spaces. With intl, the output can contain a no-break space (U+00A0, for example between the digit groups in de-AT) or a narrow no-break space (U+202F, for example in fr-FR) where it looks like an ordinary space. Compare with those characters, or normalise them first, when a test or a parser reads the result.
perflocale()->format_currency( $value, string $currency_code, ?string $lang_slug = null ): string
Locale-aware currency formatting via NumberFormatter::CURRENCY. Honours the locale's symbol position, fraction digits, and grouping conventions for the supplied ISO 4217 code. $lang_slug takes the same values as in format_number().
perflocale()->format_currency( 19.99, 'USD' ); // "$19.99"
perflocale()->format_currency( 19.99, 'EUR', 'de' ); // "19,99 €" (U+00A0 before the symbol)
perflocale()->format_currency( 1234, 'JPY' ); // "¥1,234" (zero decimals)Without intl loaded, both functions still return useful output. The fallback keeps decimals - format_number( 1234.5 ) returns the same value with or without the extension. It also punctuates for the language you asked for rather than for the language the page is being rendered in: format_number( 1234.5, 'de' ) returns 1.234,5 on an English page. That uses WordPress's own separators, so it needs the target language's WordPress translation files installed; with none, the site default is used. format_currency still falls back to {CODE} {amount} rather than guessing a symbol - the amount itself is punctuated for the requested language. For a native currency symbol and position, ensure your hosting environment has ext-intl.
Switcher
perflocale()->switcher( array $args = [] ): string
Returns the language switcher HTML string. Accepts the same snake_case arguments as the [perflocale_switcher] shortcode, including untranslated_link, include and exclude; any argument omitted falls through to the global Settings → Language Switcher value.
echo perflocale()->switcher();
// Inline flags + names in the footer.
echo perflocale()->switcher( [
'display' => 'inline',
'template' => 'flags_names',
'hide_current' => true,
] );
// Compact header dropdown: "EN" pill on the button, full native names
// in the options panel, with stacked up + down chevrons.
echo perflocale()->switcher( [
'display' => 'dropdown',
'name_format' => 'native',
'trigger_format' => 'slug',
'arrow_style' => 'double',
'class' => 'site-header__lang',
] );
// Only these languages (the current one always stays), untranslated ones without a link.
echo perflocale()->switcher( [
'include' => [ 'en', 'de', 'fr' ],
'show_untranslated' => true,
'untranslated_link' => 'no_link',
] );See Language Switcher for the full argument list. The per-instance attribute coverage matrix shows how the same arguments are exposed on every other rendering surface (block, widget, shortcode, Customizer floating, theme addons, page-builder integrations).
perflocale()->current_url( string $lang_slug ): string
Returns the URL of the current page in another language, named by its slug, locale or BCP 47 tag - the same canonical URL the bundled switcher and hreflang tags use. On singular pages where the target language has no translation, returns an empty string so callers can fall through to perflocale()->home_url( $slug ) or render an “untranslated” UI as they prefer.
if ( $de_url = perflocale()->current_url( 'de' ) ) {
echo '<a href="' . esc_url( $de_url ) . '">Deutsch</a>';
}perflocale()->switcher_links(): array
Returns a structured array of switcher items for the current page - the same data the bundled switcher renders, exposed so themes can build their own markup without re-implementing URL conversion or translation-availability checks. Languages without a translation on the current singular page are still included with is_translated => false and an empty url; themes that want the bundled switcher’s hide-untranslated behaviour can filter on that flag.
Each entry has the following keys:
| Key | Type | Example |
|---|---|---|
slug | string | "fr" |
locale | string | "fr_FR" |
name | string | "French" |
native_name | string | "Français" |
flag | string | "🇫🇷" |
text_direction | string | "ltr" |
url | string | "https://example.com/fr/about/" |
is_current | bool | false |
is_translated | bool | true |
<ul class="my-lang-switcher">
<?php foreach ( perflocale()->switcher_links() as $link ) : ?>
<li class="<?php echo $link['is_current'] ? 'is-current' : ''; ?>">
<?php if ( $link['is_translated'] ) : ?>
<a href="<?php echo esc_url( $link['url'] ); ?>" hreflang="<?php echo esc_attr( $link['slug'] ); ?>">
<?php echo esc_html( $link['flag'] . ' ' . $link['native_name'] ); ?>
</a>
<?php else : ?>
<span class="is-untranslated"><?php echo esc_html( $link['native_name'] ); ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>Date & Time
Each language can override the site’s date_format / time_format from PerfLocale → Languages. These helpers return a language’s resolved format, falling back to the site default when no override is set. The optional last parameter $lang names the language by slug, locale or BCP 47 tag; null (the default) means the current language. A locale no language on the site uses gets the site default format. An unknown value raises a _doing_it_wrong() notice and the current language is used. Filterable via perflocale/date_format and perflocale/time_format.
perflocale()->date_format( ?string $lang = null ): string
Returns the PHP date-format string for a language (e.g. "j F Y").
echo wp_date( perflocale()->date_format(), get_post_time( 'U' ) );
echo perflocale()->date_format( 'de' ); // the German language's formatperflocale()->time_format( ?string $lang = null ): string
Returns the PHP time-format string for a language (e.g. "H:i").
perflocale()->datetime_format( ?string $lang = null ): string
Combined date + time format - the two strings joined with a single space.
perflocale()->format_date( int|string|DateTimeInterface|null $timestamp = null, ?string $lang = null ): string
Render a timestamp in a language’s date format, with that language’s month and day names. Accepts a Unix timestamp, a DateTimeInterface, a date string, or null for “now”. A date string without its own offset ("2026-05-13 14:00:00", as WordPress stores post dates) is read in the site’s time zone; one with an offset ("2026-05-13T14:00:00+02:00") keeps it. Returns an empty string if the input can’t be parsed. Wraps wp_date(), so the result is in the site’s time zone.
Month and day names of a language other than the page’s come from that locale’s WordPress language pack; without it installed, the page language’s names are used.
echo perflocale()->format_date( get_post_time( 'U' ) );
// French: "13 mai 2026"
// English: "May 13, 2026"
echo perflocale()->format_date( $post->post_date, 'de' );
// The German language's format and month names, on any pageperflocale()->format_time( int|string|DateTimeInterface|null $timestamp = null, ?string $lang = null ): string
Same as format_date() but applies the time format.
perflocale()->format_datetime( int|string|DateTimeInterface|null $timestamp = null, ?string $lang = null ): string
Combined date + time render, as format_date().
Service Container Access
For advanced use cases, the service container is reachable via \PerfLocale\Plugin::get_instance():
$plugin = \PerfLocale\Plugin::get_instance();
$router = $plugin->get( 'router' );
$settings = $plugin->get( 'settings' );
$cache = $plugin->get( 'cache' );
$converter = $plugin->get( 'url_converter' );Template Tags
These standalone template-tag functions wrap the same helpers for use directly in theme templates. Where a tag takes a language ($slug, $lang), it accepts a slug, locale or BCP 47 tag, as described under language values.
| Function | Description |
|---|---|
perflocale_language_switcher( $args ) | Echo the language switcher |
perflocale_get_language_switcher( $args ) | Return the language switcher HTML |
perflocale_current_language() | Get current language object |
perflocale_get_languages() | Get all active languages |
perflocale_get_permalink( $id, $slug ) | Get translated post URL |
perflocale_get_term_link( $id, $slug ) | Get translated term URL |
perflocale_home_url( $slug, $path ) | Get language home URL |
perflocale_current_url( $slug ) | Current page’s URL in another language (empty when untranslated) |
perflocale_switcher_links() | Structured switcher items - same shape as perflocale()->switcher_links() |
perflocale_is_rtl() | Check if current language is RTL; without one, WordPress’s is_rtl() |
perflocale_if_language( $langs, $invert = false ) | Conditional - true when the current language matches (single slug, comma-separated string, or array). Same predicate that backs the Gutenberg block and [perflocale_if] shortcode. Before plugins_loaded it returns false, also with $invert. |
perflocale_t( $key, $context = '', $language = '' ) | Translate a string registered under the perflocale domain. Returns the original key if no translation is found. |
perflocale_date_format( $lang = null ) | Date format for a language (default: the current one) |
perflocale_time_format( $lang = null ) | Time format for a language (default: the current one) |
perflocale_format_date( $timestamp = null, $lang = null ) | Render a timestamp as a date in a language (default: the current one) |
perflocale_format_time( $timestamp = null, $lang = null ) | Render a timestamp as a time in a language (default: the current one) |
perflocale_format_datetime( $timestamp = null, $lang = null ) | Render a timestamp as date + time in a language (default: the current one) |
perflocale_hreflang_tags() | Output hreflang tags manually. They print once per request: after the automatic output in wp_head, a call prints nothing, and after a call the automatic output does not repeat them. |
Examples
Conditional content by language
if ( perflocale()->is_language( 'fr' ) ) {
get_template_part( 'partials/hero', 'fr' );
} else {
get_template_part( 'partials/hero' );
}Custom language menu
<ul class="lang-menu">
<?php foreach ( perflocale()->languages() as $lang ) : ?>
<li class="<?php echo perflocale()->is_language( $lang->slug ) ? 'active' : ''; ?>">
<a href="<?php echo esc_url( perflocale()->home_url( $lang->slug ) ); ?>">
<?php echo esc_html( $lang->native_name ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>Show translation links for current post
<?php
$translations = perflocale()->translations( get_the_ID(), true ); // published only
foreach ( $translations as $slug => $post_id ) :
if ( $post_id === get_the_ID() ) continue;
?>
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
Read in <?php echo esc_html( strtoupper( $slug ) ); ?>
</a>
<?php endforeach; ?>