RTL Language Support
RTL (right-to-left) language support hooks into WordPress’s built-in text direction system rather than reimplementing it. This document explains the code path from language detection through to WordPress’s is_rtl() returning true, and the edge cases around inactive language previews.
RTL Metadata Source
Section titled “RTL Metadata Source”File: config/languages.jsonc
The authoritative RTL flag for each language lives in the languages.jsonc config file, not in the database or WordPress locale metadata. Each language entry has an rtl boolean field. ConfigService loads this file and exposes values via config->get("languages.{$code}.rtl", false).
Languages without an explicit rtl key default to false. The lookup key uses the language code (e.g., ar, he_IL, fa_IR), not the slug.
Detection: LocaleService::isCurrentLanguageRtl()
Section titled “Detection: LocaleService::isCurrentLanguageRtl()”File: src/Service/LocaleService.php
public function isCurrentLanguageRtl(): bool{ $code = $this->getDetectedLanguageCode(); if (!$code) { return false; } return (bool) ($this->config->get("languages.{$code}.rtl", false));}getDetectedLanguageCode() checks three things in order:
$this->pendingInactiveLanguage['code']— if an inactive language is in the pending-auth state (see below), its code is returned here so RTL detection works beforefinalizeLanguageDetection()promotes it.$this->currentLanguage['code']— the active language after detection.- Returns
nullif no language has been detected yet.
This means isCurrentLanguageRtl() returns the correct value at any point in the boot sequence, including during plugins_loaded before authentication context is available.
WordPress Integration: RewriterSubscriber::filterTextDirection()
Section titled “WordPress Integration: RewriterSubscriber::filterTextDirection()”File: src/Subscriber/RewriterSubscriber.php
public function filterTextDirection(string $translation, string $text, string $context, string $domain): string{ if ($context === 'text direction' && $text === 'ltr') { if ($this->locale->isCurrentLanguageRtl()) { return 'rtl'; } } return $translation;}This method is registered on the gettext_with_context filter at priority 10 (see hooks-filters.mdx for the full subscriber hook list). WordPress calls _x('ltr', 'text direction') inside WP_Locale::init(). By intercepting that specific call and returning 'rtl', the plugin causes WP_Locale::$text_direction to be set to 'rtl'.
Once $text_direction is 'rtl', all of WordPress’s RTL machinery activates automatically:
is_rtl()returnstrue<html dir="rtl">is output- RTL stylesheets (files ending in
-rtl.css) are enqueued by themes and plugins that support them body_classincludes RTL-related classes
No WordPress core code is duplicated. The plugin intercepts a single gettext call.
Inactive Language RTL Preview
Section titled “Inactive Language RTL Preview”File: src/Service/LocaleService.php
Language detection runs at plugins_loaded priority 1, before WordPress has loaded the current user’s capabilities. If the detected language is marked inactive in settings, detectLanguage() does not activate it immediately. Instead it stores the language data in $pendingInactiveLanguage:
$this->pendingInactiveLanguage = $language; // deferred until initfinalizeLanguageDetection() runs on init and checks access->canViewInactiveLanguages(). If authorized, it calls setLanguage() to promote the pending language to active. If not, it reverts to the default.
The pendingInactiveLanguage check in getDetectedLanguageCode() ensures that RTL detection — and filterTextDirection() — works correctly during this pending phase. An admin previewing an inactive Arabic page gets dir="rtl" on the <html> tag even before finalizeLanguageDetection() runs, because the filter fires during the same request after detectLanguage() has set pendingInactiveLanguage.
LanguageSwitcherService: is_rtl Field
Section titled “LanguageSwitcherService: is_rtl Field”File: src/Service/LanguageSwitcherService.php
Each language item in the switcher data array includes an is_rtl boolean:
'is_rtl' => (bool) $this->config->get("languages.{$code}.rtl", false),This is read directly from config/languages.jsonc, the same source as LocaleService. The field is used by the frontend to render directional arrows or layout adjustments in the language switcher widget. It is separate from the filterTextDirection() mechanism — the switcher only needs to know about RTL for its own rendering, not to drive WordPress’s direction system.
Debugging RTL Issues
Section titled “Debugging RTL Issues”RTL not activating (no dir="rtl" on <html>):
- Confirm the language code has
"rtl": trueinconfig/languages.jsonc. UseConfigService::get("languages.{$code}.rtl")from a custom debugging action to verify. - Confirm
RewriterSubscriberis registered. CheckPlugin::registerSubscribers()—RewriterSubscriberis first in the list. - Confirm
LocaleService::detectLanguage()ran and set the correct language. Add temporary logging toisCurrentLanguageRtl()to see what code is being looked up. - If the language is inactive, verify the previewing user has
omnalingo_view_inactiveoredit_postscapability, and thatfinalizeLanguageDetection()ran (it is hooked toinitviaRewriterSubscriber).
RTL activates on the source language (everything goes RTL):
This would mean isCurrentLanguageRtl() is returning true when it should not. Check whether $pendingInactiveLanguage is being set incorrectly during a non-translated request — for example, if a URL segment coincidentally matches an RTL language slug.
RTL stylesheet not loading:
filterTextDirection() only drives is_rtl(). Whether RTL stylesheets are actually enqueued depends entirely on the theme and plugins. Check whether the active theme has an -rtl.css file. WordPress only loads it if is_rtl() is true AND the stylesheet was enqueued with wp_style_add_data($handle, 'rtl', 'replace') or a .min.css/-rtl.css naming convention. Omnalingo has no control over this.