Skip to content

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.


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:

  1. $this->pendingInactiveLanguage['code'] — if an inactive language is in the pending-auth state (see below), its code is returned here so RTL detection works before finalizeLanguageDetection() promotes it.
  2. $this->currentLanguage['code'] — the active language after detection.
  3. Returns null if 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() returns true
  • <html dir="rtl"> is output
  • RTL stylesheets (files ending in -rtl.css) are enqueued by themes and plugins that support them
  • body_class includes RTL-related classes

No WordPress core code is duplicated. The plugin intercepts a single gettext call.


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 init

finalizeLanguageDetection() 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.


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.


RTL not activating (no dir="rtl" on <html>):

  1. Confirm the language code has "rtl": true in config/languages.jsonc. Use ConfigService::get("languages.{$code}.rtl") from a custom debugging action to verify.
  2. Confirm RewriterSubscriber is registered. Check Plugin::registerSubscribers()RewriterSubscriber is first in the list.
  3. Confirm LocaleService::detectLanguage() ran and set the correct language. Add temporary logging to isCurrentLanguageRtl() to see what code is being looked up.
  4. If the language is inactive, verify the previewing user has omnalingo_view_inactive or edit_posts capability, and that finalizeLanguageDetection() ran (it is hooked to init via RewriterSubscriber).

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.