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 ships an automated axe-core audit suite as a regression guard.

Click the trigger or focus it and press Enter — full keyboard handling.
Last action
Trigger focused. Press Enter or Space to open.

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 / Space opens the panel from the trigger button
  • ArrowDown / ArrowUp moves focus between languages
  • Home / End jumps to the first or last language
  • Escape closes the panel and returns focus to the trigger
  • Tab moves 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.

Rendered HTML What assistive tech actually sees
<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.
Every ARIA string runs through WordPress i18n (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 language
  • aria-haspopup="listbox", aria-expanded, aria-controls on the trigger button
  • aria-selected on 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.

1
In the HTML markup
First thing every browser parses
<html lang="de-DE">
2
In the HTTP response header
Picked up before any HTML is parsed
HTTP/1.1 200 OK
Content-Language: de-DE
Voice profile auto-switches
Pronunciation, prosody, and stress now match Deutsch
  • JAWS
  • NVDA
  • VoiceOver
  • Orca
  • TalkBack
Filter perflocale/content_language/value customise the value — or remove the filter 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 - filter perflocale/content_language/value customises the value, removing the filter entirely disables the header.

Acme Bakery
  • Home
  • Shop
  • About
$48.00
  • <html lang="en-US"> via language_attributes()
  • style.css theme 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 by language_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). The axe-core audit suite tests the /ar/ URL explicitly so any regression in RTL rendering breaks the release.

Emitted CSS on every page with View Transitions
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 240ms;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0ms;
  }
}
OS: full motion
Default for most users
240 ms crossfade
OS: prefers-reduced-motion
Vestibular disorders, motion sensitivity
0 ms · instant page swap
WCAG 2.1 SC 2.3.3 — AAA No WordPress config needed. The browser reads the OS preference and applies the @media rule automatically.
Filter 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.

Emitted in <head> on every translated page
<link rel="alternate" hreflang="en-US"/>
<link rel="alternate" hreflang="de"/>
<link rel="alternate" hreflang="fr"/>
<link rel="alternate" hreflang="x-default"/>
  • Search engines
    Primary signal — Google, Bing, Yandex
  • Browser extensions
    Translation hubs, language sniffers
  • Reader modes
    Firefox Reader · Safari Reader
  • Some screen readers
    Offer preferred-language version if available
HreflangCoordinator
Suppresses duplicate emission when an SEO plugin is active — assistive tech sees one canonical set, never conflicting alternates.
  • 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. The plugin's HreflangCoordinator also suppresses duplicate hreflang emission when an SEO plugin (Yoast, Rank Math, AIOSEO, SEOPress, Slim SEO, or The SEO Framework) is active, so assistive tech isn't confused by conflicting alternates.

Test matrix 4 pages × 3 topologies = 12 audits per release
Single-siteMS subdomainMS subdir
Homepage default LTR
/de/ LTR non-default
/ar/ RTL
Switcher test dropdown + block
Rule packs
  • WCAG 2 A
  • WCAG 2.1 AA
  • Best practice
39/39 passing 0 failing
Any serious or critical regression blocks the release.

Automated axe-core Audits as a Release Gate

Every release is gated on axe-core audits of the plugin's rendered output. The end-to-end accessibility suite runs axe-core via headless Chromium against four pages on every site:

  • The homepage (default LTR language)
  • A /de/ page (LTR non-default - catches regressions tied to URL-prefix routing)
  • A /ar/ page (RTL language - catches layout/direction regressions)
  • A switcher-focused test page (exercises dropdown + block markup together)

Violations are filtered to those rooted at PerfLocale-emitted markup (theme issues are reported upstream but don't fail the gate). The plugin must have zero serious or critical violations under WCAG 2 A, AA, WCAG 2.1 AA, and best-practice rule packs - any regression blocks release. The suite runs on all 3 test topologies (single-site, multisite subdomain, multisite subdirectory) so routing quirks that might alter markup don't sneak in. Every release passes with zero serious or critical violations across the full topology matrix.

What we claimPerfLocale’s job

  • Rendered markup aligns with WCAG 2.1 Level AA.
  • Automated axe-core regression suite on every release.
  • Manual testing with NVDA + VoiceOver each release.

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.

Found a regression in PerfLocale’s markup?
It’s a release-blocking bug for us — please report it.

Compliance Posture

What we claim: PerfLocale's rendered markup aligns with WCAG 2.1 Level AA. We ship an automated axe-core regression suite and manually test with NVDA + VoiceOver across each release.

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.

← Back to Features