<html lang="de-DE">Accessibility
Multilingual shouldn't mean less accessible. PerfLocale aligns with WCAG 2.1 AA across every surface it renders - language switcher, admin pages, SEO markup, View Transitions, and RTL support - and its rendered markup is built and reviewed against WCAG 2.1 AA.
- English (US)
- English (UK)
- Deutsch
- Français
- العربية
Keyboard-Navigable Language Switcher
Every switcher variant - block, shortcode, nav menu, and admin bar - is fully keyboard navigable. The dropdown variant implements the WAI-ARIA listbox pattern:
Enter/Spaceopens the panel from the trigger buttonArrowDown/ArrowUpmoves focus between languagesHome/Endjumps to the first or last languageEscapecloses the panel and returns focus to the triggerTabmoves focus out of the listbox without committing a selection
Focus is managed programmatically on open/close so the next keystroke goes to the right element. Click-outside closes the panel cleanly. Tested across all 3 switcher modes - inline, simple list, and dropdown.
<nav aria-label="Language switcher">
<button aria-haspopup="listbox"
aria-expanded="false"
aria-controls="lang-list">English (US) ▾</button>
<ul id="lang-list" role="listbox">
<li role="option" aria-selected="true"
aria-current="true">English (US)</li>
<li role="option">English (UK)</li>
<li role="option" tabindex="-1">Italiano <!-- untranslated --></li>
</ul>
</nav>- LandmarkSR users jump here from the rotor / landmarks list.
- Popup stateTrigger announces “collapsed listbox” / “expanded” live.
- RolesDescribes the widget pattern (listbox + option).
- SelectionCurrent language is announced as “selected, current”.
- Tab orderUntranslated stubs stay announceable but unfocusable.
esc_attr__()) — visitors hear them in their language, not yours.ARIA Markup & Landmarks
The switcher emits a proper <nav aria-label="Language switcher"> landmark so screen-reader users can jump straight to it with landmark navigation (VoiceOver rotor, NVDA/JAWS landmarks list). Current-language links carry aria-current="true" so assistive tech announces which language is active. Inside the dropdown:
role="listbox"on the panel,role="option"on each languagearia-haspopup="listbox",aria-expanded,aria-controlson the trigger buttonaria-selectedon the current-language option- Untranslated-language options get
tabindex="-1"(removed from tab order) rather than being hidden - they remain announceable but unselectable, matching the ARIA listbox disabled-option convention
Every ARIA string flows through WordPress i18n (esc_attr__ + translators comments) so assistive tech announces the switcher in the visitor's language, not yours.
HTTP/1.1 200 OK
Content-Language: de-DE- JAWS
- NVDA
- VoiceOver
- Orca
- TalkBack
perflocale/content_language/value customise the value — return an empty string to disable the header.<html lang> & Content-Language
Screen readers change pronunciation based on the page's declared language. PerfLocale sets <html lang> to the current language's BCP-47 tag on every frontend page (e.g. lang="de-DE", lang="ar-SA"). JAWS, NVDA, VoiceOver, Orca, and TalkBack all use this attribute to switch voice profile.
On top of that, the plugin emits the W3C-standard Content-Language HTTP response header on every request, also BCP-47 normalised. This is a second signal honoured by proxies, CDNs, and some assistive tech. Turn it off if your CDN mangles the header - the perflocale/content_language/value filter customises the value, and returning an empty string from it (or switching off the Content-Language setting) disables the header.
<html lang="en-US">vialanguage_attributes()style.csstheme stylesheet (LTR variant)perflocale()->is_rtl()helper for template branching
Right-to-Left (RTL) Language Support
Arabic, Hebrew, Persian, Urdu, and other RTL languages are first-class. Every language row in the Languages settings page has a text_direction field (ltr or rtl); when a visitor lands on an RTL language's URL prefix, the plugin filters WordPress's locale to the RTL locale - from there, WP core applies all its standard RTL behaviour automatically, including:
<html dir="rtl">emitted bylanguage_attributes()- Automatic loading of the theme's RTL stylesheet (
style-rtl.css) if present - Core + admin stylesheets flipped to RTL variants
On top of that, the plugin emits the correct BCP-47 language code in its own hreflang + Content-Language output (e.g. ar-SA, he-IL). Integrators can check perflocale()->is_rtl() in templates to branch layout for mixed-direction content (e.g. an English blog with Arabic comments).
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .24s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0s;
animation: none;
}
}prefers-reduced-motion@media rule automatically.perflocale/view_transitions/css Customise the CSS — keep the reduced-motion guard.Respects prefers-reduced-motion
When View Transitions are enabled, the 240ms cross-document crossfade is automatically suppressed for users with the OS-level reduced-motion preference. The emitted CSS includes a @media (prefers-reduced-motion: reduce) block that zeros the animation duration - navigation still completes instantly (users still get the instant feel of the speculation-rules prerender), but the crossfade itself is skipped.
This aligns with WCAG 2.1 SC 2.3.3 Animation from Interactions (AAA), protecting users with vestibular disorders or motion sensitivity. Users don't need to configure anything on the WordPress side - the browser already knows their preference from the OS and the CSS reads it automatically.
If you customise the View Transitions CSS via the perflocale/view_transitions/css filter, keep the reduced-motion guard in your replacement block to preserve compliance.
<link rel="alternate" hreflang="en-US" …/>
<link rel="alternate" hreflang="de" …/>
<link rel="alternate" hreflang="fr" …/>
<link rel="alternate" hreflang="x-default" …/>- Search enginesPrimary signal — Google, Bing, Yandex
- Browser extensionsTranslation hubs, language sniffers
- Reader modesFirefox Reader · Safari Reader
- Some screen readersOffer preferred-language version if available
- Yoast
- Rank Math
- AIOSEO
- SEOPress
- Slim SEO
- The SEO Framework
Hreflang for Assistive Tech Discoverability
PerfLocale emits <link rel="alternate" hreflang="..."> tags for every available translation of the current page, plus hreflang="x-default". While hreflang is primarily a search-engine signal, it's also consumed by browser extensions, reader modes (Firefox Reader, Safari Reader), and some screen readers to offer the visitor their preferred-language version if one exists. There is only ever one set on the page: none of the supported SEO plugins (Yoast, Rank Math, AIOSEO, SEOPress, Slim SEO, The SEO Framework) emit head or header hreflang without a multilingual integration of their own, so assistive tech is never confused by conflicting alternates.
What we claimPerfLocale’s job
- Rendered markup aligns with WCAG 2.1 Level AA.
What we don’t claimSite-wide · auditor’s call
- Theme accessibility · colour contrast · image alt text
- Content quality · heading order · reading flow
- Third-party plugins · external embeds
A multilingual plugin can hand you compliant plumbing — rendered-page compliance is a site-wide property your auditor verifies.
Compliance Posture
What we claim: PerfLocale's rendered markup aligns with WCAG 2.1 Level AA. Landmarks, language codes, focus management and motion guards are implemented against those success criteria.
What we don't claim: we can't guarantee your finished site is WCAG compliant - that also depends on your theme, your content, your images' alt text, your colour choices, and any third-party plugins. A multilingual plugin can hand you compliant plumbing (landmarks, language codes, focus management, motion guards); compliance of the rendered page is a site-wide property you verify with your own auditor.
If you find an accessibility regression in PerfLocale's own markup, please report it - it's a release-blocking bug for us.