Language Switcher
PerfLocale provides multiple ways to display a language switcher on your site.
Gutenberg Block
Add the Language Switcher block from the block inserter (search "Language Switcher" or find it under Widgets).
Block Settings
| Setting | Options | Default |
|---|---|---|
| Style | Flags + Names, Flags Only, Names Only | Flags + Names |
| Layout | Horizontal, Vertical | Horizontal |
| Name Format | Native, English, Both - English (Native), Code | Native |
| Trigger Label Format (dropdown only) | Match Name Format, Native, English, Both, Code only - controls the dropdown button's visible label independently of the options' format. Use Code only to get a compact "EN" pill in the header while the dropdown lists full names. Hidden when Display Mode is not Dropdown. | Match Name Format |
| Show flags | On/Off | On |
| Show names | On/Off | On |
| Hide current | On/Off | On |
| Show untranslated | On/Off | Off |
| Dropdown arrow | Single chevron, Double chevrons, None (dropdown style only). Theme override via perflocale/switcher/arrow_html filter. | Single chevron |
| Font size | 10–24 px | 14 |
| Flag size | 14–40 px | 20 |
| Gap | 0–24 px | 8 |
The block uses server-side rendering, so you see a live preview in the editor.
The block also honors WordPress's standard block supports on the frontend - Align (left / center / right / wide / full), Spacing (padding / margin), Color, Typography, and Additional CSS class - so the same wrapper classes you see in the editor reach the page. Themes' is-layout-constrained layout rules cannot stretch the dropdown trigger to the full content-area width; the wrapper stays sized to the visible button.
Shortcode: [perflocale_switcher]
Place this shortcode in any post, page, or widget area.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
style | flags_names, flags_only, names_only | From settings | Display style |
display | inline, simple, dropdown | From settings | Layout mode. dropdown renders a click-to-expand button + listbox panel. |
layout | horizontal, vertical | horizontal | List direction |
name_format | native, english, both, slug | native | How to display language names in the options list |
trigger_format | inherit, native, english, both, slug | inherit | Dropdown trigger button label format. inherit reuses name_format; an explicit value lets the header pill diverge from the options (e.g. slug trigger + native options renders "EN" on the button + "Deutsch / Français" inside the dropdown). Only meaningful when display="dropdown". |
arrow_style | single, double, none | From settings | Dropdown trigger chevron icon. single = one down chevron (classic native-select look); double = stacked up + down chevrons; none = no icon (theme custom icons via the perflocale/switcher/arrow_html filter). Only meaningful when display="dropdown". |
show_flags | 1 / 0 | 1 | Show flag emoji |
show_names | 1 / 0 | 1 | Show language names |
hide_current | 1 / 0 | From settings | Hide the active language |
show_untranslated | 1 / 0 | From settings | Show languages without a translation |
untranslated_link | homepage, no_link, hide | From settings | What a language without a translation shows when show_untranslated is on: a link to that language’s home page, its name without a link, or nothing. |
include | Comma-separated slugs, locales or BCP 47 tags | (empty: all) | Show only these languages. The current language always stays (hide_current still hides it). |
exclude | Comma-separated slugs, locales or BCP 47 tags | (empty) | Leave these languages out. The current language always stays. |
class | CSS class name | (empty) | Custom CSS class |
A value in include or exclude that names no active language is ignored, with a _doing_it_wrong() notice (shown with WP_DEBUG on). When fewer than two languages are left, the switcher renders nothing. In PHP (perflocale_language_switcher() and perflocale()->switcher()), both also take an array.
Examples
[perflocale_switcher]
[perflocale_switcher display="dropdown"]
[perflocale_switcher style="flags_only" hide_current="1"]
[perflocale_switcher style="names_only" name_format="english"]
[perflocale_switcher layout="vertical" show_flags="0"]
[perflocale_switcher style="flags_names" name_format="both" class="my-switcher"]
[perflocale_switcher display="dropdown" arrow_style="double" trigger_format="slug"]
[perflocale_switcher include="en,de,fr"]
[perflocale_switcher exclude="pt-BR" show_untranslated="1" untranslated_link="no_link"]Widget
Go to Appearance > Widgets and add the Language Switcher widget (in the PerfLocale section, or via the Legacy Widget block). The widget exposes the full set of per-instance options - Display Mode, Style, Layout, Name Format, Trigger Label Format, Dropdown Arrow, Show flags / names, Hide current, Show untranslated, Font / Flag / Gap sizes - matching the Gutenberg block one-for-one. The dropdown-only fields (Trigger Label Format, Dropdown Arrow) default to "Use site default" so existing widget instances keep the global Settings → Language Switcher value until you explicitly override.
PHP Template Tag
In your theme templates:
<?php perflocale_language_switcher(); ?>
<?php perflocale_language_switcher( [
'template' => 'flags_names',
'display' => 'dropdown',
'show_flags' => true,
'show_names' => true,
'hide_current' => false,
'name_format' => 'native',
'trigger_format' => 'slug', // "EN" on the trigger button
'arrow_style' => 'double', // up + down chevrons
'class' => 'my-custom-switcher',
] ); ?>
<?php perflocale_language_switcher( [
'exclude' => [ 'pt_BR' ], // slug, locale or BCP 47 tag
'show_untranslated' => true,
'untranslated_link' => 'hide', // or 'homepage', 'no_link'
] ); ?>Per-instance attribute coverage across surfaces
Every rendering surface - block, shortcode, template tag, WP widget, page-builder integrations, theme addons, the floating Customizer switcher - supports the same set of per-instance overrides. Unset / empty values fall through to the global Settings → Language Switcher defaults, so you can configure the site-wide look once and only override specific instances when needed.
| Surface | Trigger Label Format | Dropdown Arrow |
|---|---|---|
| Gutenberg Block | Inspector → Display | Inspector → Appearance |
Shortcode [perflocale_switcher] | trigger_format="…" | arrow_style="…" |
PHP Template Tag perflocale_language_switcher() | 'trigger_format' => '…' | 'arrow_style' => '…' |
| WP Widget (Appearance → Widgets) | Trigger Label Format dropdown | Dropdown Arrow dropdown |
| Elementor widget | Trigger Label Format select (dropdown-only) | Dropdown Arrow select (dropdown-only) |
| Beaver Builder module | Trigger Label Format select | Dropdown Arrow select |
| Bricks element | Trigger Label Format select (dropdown-only) | Dropdown Arrow select (dropdown-only) |
| Customizer floating switcher | perflocale_floating_trigger_format control | perflocale_floating_arrow_style control |
| Blocksy header + footer language switcher | Trigger Label Format option (dropdown-only) | Dropdown Arrow option (dropdown-only) |
| Kadence header switcher | perflocale_kadence_trigger_format Customizer control | perflocale_kadence_arrow_style Customizer control |
| Neve header builder component | Trigger Label Format component setting | Dropdown Arrow component setting |
The dropdown-only controls (Trigger Label Format + Dropdown Arrow) are hidden in the UI when the surface's Display Mode is set to inline / simple - there's no trigger button to format in those modes. If you set them anyway (e.g. via shortcode attribute), the block render silently ignores them outside dropdown mode, so it's safe to script.
Customising the switcher output
The switcher HTML is generated in PHP (src/Frontend/LanguageSwitcher.php and LanguageSwitcherBlock.php) - there are no PHP templates to copy into your theme. Customise it via filters and CSS instead:
// 1. Filter the language list before rendering (e.g. hide a language).
add_filter( 'perflocale/switcher/languages', function( $languages ) {
return array_filter( $languages, fn( $lang ) => $lang->slug !== 'de' );
} );
// 2. Filter the <a> attributes for each link (add data attributes, classes, etc.).
add_filter( 'perflocale/switcher/link_attrs', function( $attrs, $lang, $current_slug, $args ) {
$attrs['data-track'] = 'lang-switch:' . $lang->slug;
return $attrs;
}, 10, 4 );
// 3. Inject HTML at the start / end of the dropdown panel.
// Useful for region groupings, search inputs, branding chrome, etc.
// Returned HTML is sanitised through kses_switcher() - extend the
// allowlist with perflocale/switcher/kses_allowed_html if needed.
add_filter( 'perflocale/switcher/panel_before', function( $html, $languages, $current_slug, $attrs ) {
return '<div role="presentation" class="my-switcher-heading">Choose your language</div>';
}, 10, 4 );
// 4. Swap the dropdown chevron for a custom icon.
add_filter( 'perflocale/switcher/arrow_html', function( $html, $style ) {
return '<i class="fa-solid fa-chevron-down" aria-hidden="true"></i>';
}, 10, 2 );
// 5. Filter the inner HTML of every option (flag + label).
// Same callback fires from dropdown, inline, and simple paths.
add_filter( 'perflocale/switcher/option_content', function( $inner, $lang ) {
// Wrap in <bdi> for safe mixed-direction rendering of e.g.
// "العربية" inside an LTR option list.
return '<bdi>' . $inner . '</bdi>';
}, 10, 2 );
// 6. Filter the entire rendered HTML as a last-resort override.
add_filter( 'perflocale/switcher/output', function( $html, $args ) {
// Wrap the switcher in your own container, swap markup, etc.
return '<div class="my-switcher-wrap">' . $html . '</div>';
}, 10, 2 );Built-in CSS class hooks (target these in your theme stylesheet to restyle without touching markup):
.perflocale-switcher-block- root nav element on the block/shortcode switcher.perflocale-switcher-block--dropdown/.perflocale-switcher-block--horizontal/.perflocale-switcher-block--vertical/.perflocale-switcher-block--simple- mode modifier (set from the block’s Display attribute).perflocale-switcher-block__item,.perflocale-switcher-block__item--current,.perflocale-switcher-block__label,.perflocale-switcher-block__flag- per-language item internals (used by horizontal / vertical / simple modes).perflocale-dd__anchor- inline-block wrapper inside the dropdown<nav>that shrink-wraps to the trigger button. The panel anchors here (not the wider<nav>) somin-width: 100%on the panel correctly means "at least as wide as the trigger"..perflocale-dd__trigger,.perflocale-dd__panel,.perflocale-dd__option- dropdown internals.perflocale-dd__flag,.perflocale-dd__label,.perflocale-dd__arrow- dropdown content slots
CSS custom properties
The dropdown surface exposes every colour, spacing, timing, and z-index value as a --perflocale-dd-* custom property with a hardcoded fallback. A theme recolours / resizes the switcher by overriding the relevant vars instead of restating rules; the matching selector specificity is (0,2,0), so any (0,2,0)-or-higher selector wins.
/* Dark-theme palette. */
.my-dark-theme .perflocale-switcher-block--dropdown {
--perflocale-dd-bg: #1f2937;
--perflocale-dd-text: #f3f4f6;
--perflocale-dd-border: #374151;
--perflocale-dd-border-hover: #4b5563;
--perflocale-dd-panel-bg: #111827;
--perflocale-dd-panel-border: #1f2937;
--perflocale-dd-option-text: #d1d5db;
--perflocale-dd-option-hover-bg: #1f2937;
--perflocale-dd-option-hover-text: #f3f4f6;
--perflocale-dd-active-bg: #312e81;
--perflocale-dd-active-text: #e0e7ff;
}
/* Tighter pill + faster open. */
.compact-header .perflocale-switcher-block--dropdown {
--perflocale-dd-trigger-padding-block: 4px;
--perflocale-dd-trigger-padding-inline: 8px;
--perflocale-dd-trigger-radius: 12px;
--perflocale-dd-transition-duration: 0.08s;
}| Group | Variables |
|---|---|
| Surface colours | --perflocale-dd-bg, --perflocale-dd-text, --perflocale-dd-border, --perflocale-dd-border-hover, --perflocale-dd-focus-border, --perflocale-dd-focus-ring, --perflocale-dd-panel-bg, --perflocale-dd-panel-border, --perflocale-dd-panel-shadow |
| Option states | --perflocale-dd-option-text, --perflocale-dd-option-hover-bg, --perflocale-dd-option-hover-text, --perflocale-dd-active-bg, --perflocale-dd-active-text, --perflocale-dd-active-hover-bg |
| Trigger spacing | --perflocale-dd-trigger-padding-block, --perflocale-dd-trigger-padding-inline, --perflocale-dd-trigger-gap, --perflocale-dd-trigger-radius, --perflocale-dd-trigger-border-width, --perflocale-dd-trigger-focus-ring |
| Panel spacing | --perflocale-dd-panel-offset (gap below trigger), --perflocale-dd-panel-radius, --perflocale-dd-panel-border-width, --perflocale-dd-panel-padding-block, --perflocale-dd-panel-padding-inline, --perflocale-dd-panel-z-index |
| Option spacing | --perflocale-dd-option-padding-block, --perflocale-dd-option-padding-inline, --perflocale-dd-option-gap |
| Arrow / timing | --perflocale-dd-arrow-size (em), --perflocale-dd-arrow-opacity, --perflocale-dd-transition-duration, --perflocale-dd-option-transition-duration |
| Per-instance (set inline by the block / shortcode) | --perflocale-font-size, --perflocale-flag-size, --perflocale-gap |
All transitions on .perflocale-dd__trigger, .perflocale-dd__arrow, and .perflocale-dd__option are disabled inside a @media (prefers-reduced-motion: reduce) block, so users who opt out at the OS level don't see the cosmetic fades / arrow rotation.
RTL and logical properties
The dropdown CSS uses logical properties throughout (inset-inline-start instead of left, padding-block / padding-inline instead of padding: 7px 12px, etc.). The panel anchors to the trigger's inline-start edge on LTR pages and inline-end on RTL pages without a separate RTL stylesheet, and the horizontal overflow flip in language-switcher-dropdown.js reads getComputedStyle(panel).direction so it measures viewport space against the right physical edge regardless of writing direction.
Accessibility (lang, dir, ARIA)
Every option link, the dropdown trigger button, the disabled-option span, and the current-language span carry lang with the target language’s BCP 47 tag, taken from its locale as in the page’s hreflang tags (for example lang="pt-BR"; the slug when the language has no locale), and each link carries the same tag in hreflang, plus dir="rtl" when the target language's text_direction is RTL. The visible text inside each option is in the TARGET language (Français, العربية), not the page language, so screen readers need lang to pronounce labels with the right phonemes, and the browser needs dir="rtl" to render RTL labels right-to-left even on an LTR page. The perflocale/switcher/link_attrs filter still lets you override these per-link if needed.
The dropdown trigger uses aria-haspopup="listbox", aria-expanded, and aria-controls. The panel uses role="listbox" + aria-labelledby pointing at the trigger. Each option carries role="option" + aria-selected="true" on the current language; untranslated options that can't be activated carry aria-disabled="true" and are skipped by the JS arrow-key navigation. Keyboard: Enter / Space / ArrowDown opens + focuses option 1, ArrowUp opens + focuses the last option, arrows / Home / End move focus inside the panel (wrapping), a single printable character does first-letter type-ahead (mirrors native <select>), and Esc closes the panel and returns focus to the trigger.
The switcher’s own texts, its accessible name Language switcher, the dropdown button’s label Choose language, currently … and the No languages configured. message, ship translated into 24 languages: Arabic, Bulgarian, Czech, German, Spanish, Persian, Finnish, French, Hindi, Indonesian, Italian, Japanese, Korean, Dutch, Polish, Portuguese (Brazil), Portuguese (Portugal), Russian, Swedish, Turkish, Ukrainian, Vietnamese, Chinese (China) and Chinese (Taiwan). WordPress loads them on version 6.6 or newer. Regional variants without a catalog of their own use the closest bundled language: Austrian and Swiss German and formal German use German, Belgian and formal Dutch use Dutch, every French and Spanish variant uses French or Spanish, and Portuguese (Angola) and Portuguese (Portugal, AO90) use Portuguese (Portugal). When a WordPress.org language pack for PerfLocale is installed for a locale, WordPress uses it instead. English locales show the original texts, and other locales show them in English until a language pack exists.
Live repositioning
While the panel is open, scroll (capture, passive), resize, and a ResizeObserver on the trigger button re-run the panel's positioning so it doesn't drift off-screen when the page reflows (sticky-header scrolls, viewport rotation, mobile browser chrome show/hide, web-font swap). The vertical anchor flips above if there's not enough room below (accounting for the WP admin bar); the horizontal anchor flips to inline-end if the inline-start anchor would push the panel past the viewport edge. Listeners attach on open and detach on close. When the trigger button is hidden while the panel is open (for example a responsive layout hiding the header switcher), the panel closes, its listeners detach and perflocale:switcher:close fires, so keyboard focus is never left inside a hidden list.
When JavaScript does not run, the dropdown still works: the trigger button is hidden and the language links show as a plain row, each reachable with Tab.
JavaScript events
The dropdown switcher dispatches five namespaced CustomEvents on its <nav> root so external JavaScript (analytics, A/B testing tools, theme scripts, headless integrations) can observe and influence it without forking the renderer. All of them bubble, so a listener on the <nav> or on any ancestor (including document) hears every switcher on the page. Each event's full event.detail table is on its card in the Hooks Reference.
| Event | When it fires | Cancelable |
|---|---|---|
perflocale:switcher:ready | Once per switcher, after its set-up. The nav then has data-perflocale-ready="1", so a script that loads later checks that attribute instead. | No. |
perflocale:switcher:beforeopen | Before the panel opens, before anything changes: another open switcher is still open, aria-expanded is still "false" and focus has not moved. | Yes - event.preventDefault() leaves every switcher as it was and moves no focus. |
perflocale:switcher:open | After the panel opens (the open class and aria-expanded="true" are applied and positioning has run). | No. |
perflocale:switcher:close | After the panel closes, on every path. detail.source says which: click (the trigger again), escape, outside (a click outside), focusout (focus left, for example with Tab), other-switcher (another switcher opened) or hidden (the trigger stopped being displayed). detail.pointer is mouse, touch or pen when a press or click caused the close, also for a focusout right after a press outside the switcher; it is empty for Tab, a script, or a press inside the switcher. | No. |
perflocale:switcher:navigate | Before the browser follows a chosen option's link, by mouse or keyboard (Enter on a focused option dispatches the same click). An option without a link fires nothing. | Yes - event.preventDefault() keeps the visitor on the current page. |
Event detail
// Every switcher event
event.detail = {
version: 1, // later versions only add keys
nav: HTMLElement, // the switcher's <nav> root
trigger: HTMLElement, // the <button> trigger
panel: HTMLElement, // the listbox <div>
instance: string, // the nav's id, else 'perflocale-switcher-<n>' (order of set-up)
current: Language, // the page's language
languages: Option[], // the options, in display order
source: string, // what caused it: 'setup', 'click', 'keyboard' or a close source
}
// `beforeopen`, `open`, `close` and `navigate` add
pointer: string, // 'mouse', 'touch' or 'pen' for a pointer press or click; '' for a key, a script,
// or a browser that does not report the pointer type
// `perflocale:switcher:navigate` adds
target: Option, // the chosen option
from: Language, // the page's language (= current)
option: HTMLElement, // the chosen option element
url: string, // its link
slug: string, // its hreflang (a BCP 47 tag such as pt-BR); target.slug is the language slug
Language = { slug, locale, name, nativeName, isDefault } // name: the English name
Option = { ...Language, url, available, option }
// available: this page has a translation in that language (the current one counts).
// An option that links to a language's home page because the page is not
// translated has available: false; an option without a link has url: ''.current and languages are read from data-pl-* attributes on the trigger and the options the first time a listener reads them, then kept for that switcher. A page whose listeners never read them reads no attributes. A perflocale/switcher/link_attrs filter that drops those attributes leaves the matching fields empty.
Usage examples
// 1. GA4: send a `language_switch` event before the page navigates.
// `sendBeacon` flushes the request even though the page is unloading.
document.addEventListener( 'perflocale:switcher:navigate', function ( e ) {
navigator.sendBeacon(
'https://www.google-analytics.com/g/collect',
new Blob( [ JSON.stringify( {
event: 'language_switch',
from: e.detail.from.slug,
to: e.detail.target.slug,
source: e.detail.instance
} ) ], { type: 'application/json' } )
);
} );
// 2. Unsaved-changes guard: block language change if the user has
// edited a form. preventDefault() keeps them on the current page.
document.addEventListener( 'perflocale:switcher:navigate', function ( e ) {
if ( window.myAppHasUnsavedChanges ) {
if ( ! confirm( 'You have unsaved changes. Switch language anyway?' ) ) {
e.preventDefault();
}
}
} );
// 3. Keep the dropdown closed while a checkout step is in progress.
document.addEventListener( 'perflocale:switcher:beforeopen', function ( e ) {
if ( document.body.classList.contains( 'checkout-step-active' ) ) {
e.preventDefault();
}
} );
// 4. Open / close on a specific switcher only - for a sticky header
// or a hero carousel that should pause while the dropdown shows.
document.querySelector( '#header .perflocale-switcher-block--dropdown' )
.addEventListener( 'perflocale:switcher:open', function () {
document.body.classList.add( 'lang-switcher-open' );
} );
document.querySelector( '#header .perflocale-switcher-block--dropdown' )
.addEventListener( 'perflocale:switcher:close', function ( e ) {
document.body.classList.remove( 'lang-switcher-open' );
// The trigger was hidden by a responsive layout: move focus to the
// control that stands in for it.
if ( e.detail.source === 'hidden' ) {
document.querySelector( '#mobile-menu-toggle' ).focus();
}
} );Notes on scope and behaviour
- Dropdown mode only. The script that fires these events is enqueued only when the switcher renders in
dropdowndisplay mode, and thedata-pl-*attributes are only in its markup. Ininline/simplemodes the switcher is a list of<a>links with no script - attach a normalclicklistener to those links if you need to observe activations. - Bubbling. All five events bubble. Attach at the
<nav>, atdocument, or anywhere in between. The event target is always the<nav>;detail.navis included for delegated handlers. - Cancelable events.
beforeopenandnavigateare cancelable.openandclosefire after the state change, so canceling them would leave the page inconsistent with the event. - Listeners that throw. A listener that throws is reported by the browser; the switcher keeps working.
- Browsers. The script uses the
CustomEventconstructor; where it does not exist, the events are skipped rather than throwing. Every browser PerfLocale targets supports it.
Language object properties (passed into filter callbacks)
| Property | Example | Description |
|---|---|---|
slug | fr | Lowercase language code: 2–3 letters, optionally plus a hyphen and a 2–3 letter region subtag (fr, pt-br, zh-tw) |
locale | fr_FR | Full WP locale |
name | French | English name |
native_name | Français | Native name |
flag | fr | 2-letter flag country code (convert with PerfLocale\Helper::get_flag_emoji( $lang )) |
text_direction | ltr | Text direction (ltr or rtl) |
All switcher filters
The switcher exposes eight filter hooks. See the Hooks reference for full parameter signatures.
| Hook | Purpose |
|---|---|
perflocale/switcher/languages | Filter the list of languages the switcher renders. Applied on every render (not just first fetch), so context-aware shortcode wrappers can scope the list. |
perflocale/switcher/link_attrs | Filter the HTML attributes for each per-language link / span. Add analytics data-*, override hreflang / lang / dir, inject rel / title, add CSS classes. |
perflocale/switcher/option_content | Filter the inner HTML of each option (flag <span> + label <span> by default). Fires from every render surface - dropdown, inline, simple, list. Use to wrap labels in <bdi>, append region badges, prepend custom icons, etc. |
perflocale/switcher/panel_before | Inject HTML at the START of the dropdown panel, inside the listbox, before the first option. Non-interactive chrome should be wrapped in <div role="presentation"> so the parent listbox doesn't announce it as an option. |
perflocale/switcher/panel_after | Inject HTML at the END of the dropdown panel, inside the listbox, after the last option. Same role guidance as panel_before. |
perflocale/switcher/arrow_html | Override the dropdown trigger's chevron icon. Built-in styles: single (down chevron), double (stacked up/down), none (empty). Returning a non-string falls back to empty. |
perflocale/switcher/kses_allowed_html | Extend the wp_kses allowlist used by kses_switcher(). Needed when a custom arrow_html / panel_before / panel_after / option_content / link_attrs emits tags or attributes outside the built-in switcher set. |
perflocale/switcher/output | Last-resort hook on the fully-rendered HTML. Wrap, prepend, or substitute. Runs after sanitisation. |
Every filter return that produces HTML (arrow_html, panel_before, panel_after, option_content) is automatically passed through kses_switcher() at the injection point - including on the Gutenberg block render path, which WordPress's do_blocks() doesn't otherwise sanitize. The renderer's per-attribute esc_url() / esc_attr() / esc_html() calls remain in effect on the built-in HTML it emits.
Appending to a classic menu (no code)
On classic (non-block) themes, Settings → Language Switcher → Append to classic menus lists every registered classic menu location as a checkbox. Tick one and the language switcher is appended to the end of that menu automatically - no template edits, no filters. Mixed-case location slugs (e.g. headerMenu) are matched verbatim, so the append lands on the location the theme actually registered. Block themes should use the Language Switcher block instead.
Two additional hooks drive nav-menu auto-injection:
// Auto-inject the switcher items into a registered theme menu location.
// Return true to enable; pair with /switcher/menu_locations to pick which menu.
add_filter( 'perflocale/switcher/add_to_menu', '__return_true' );
// Restrict the auto-injection to specific theme menu locations.
add_filter( 'perflocale/switcher/menu_locations', function( $locations, $args ) {
return [ 'primary', 'mobile' ];
}, 10, 2 );