Add support of RTL UI in login themes (#29907)

Closes #29974

Signed-off-by: Fouad Almalki <me@fouad.io>
This commit is contained in:
Fouad Almalki 2024-06-11 14:12:13 +03:00 committed by GitHub
parent eedd167e70
commit 780ec71672
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 43 additions and 20 deletions

View file

@ -20,9 +20,11 @@ package org.keycloak.theme.beans;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
import java.text.Collator; import java.text.Collator;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -30,13 +32,18 @@ import java.util.stream.Collectors;
*/ */
public class LocaleBean { public class LocaleBean {
private static final Set<String> RTL_LANGUAGE_CODES =
Set.of("ar", "dv", "fa", "ha", "he", "iw", "ji", "ps", "sd", "ug", "ur", "yi");
private String current; private String current;
private String currentLanguageTag; private String currentLanguageTag;
private boolean rtl; // right-to-left language
private List<Locale> supported; private List<Locale> supported;
public LocaleBean(RealmModel realm, java.util.Locale current, UriBuilder uriBuilder, Properties messages) { public LocaleBean(RealmModel realm, java.util.Locale current, UriBuilder uriBuilder, Properties messages) {
this.currentLanguageTag = current.toLanguageTag(); this.currentLanguageTag = current.toLanguageTag();
this.current = messages.getProperty("locale_" + this.currentLanguageTag, this.currentLanguageTag); this.current = messages.getProperty("locale_" + this.currentLanguageTag, this.currentLanguageTag);
this.rtl = RTL_LANGUAGE_CODES.contains(current.getLanguage());
Collator collator = Collator.getInstance(current); Collator collator = Collator.getInstance(current);
collator.setStrength(Collator.PRIMARY); // ignore case and accents collator.setStrength(Collator.PRIMARY); // ignore case and accents
@ -59,6 +66,13 @@ public class LocaleBean {
return currentLanguageTag; return currentLanguageTag;
} }
/**
* Whether it is Right-to-Left language or not.
*/
public boolean isRtl() {
return rtl;
}
public List<Locale> getSupported() { public List<Locale> getSupported() {
return supported; return supported;
} }

View file

@ -58,6 +58,7 @@
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input type="text" id="totp" name="totp" autocomplete="off" class="${properties.kcInputClass!}" <input type="text" id="totp" name="totp" autocomplete="off" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>" aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"
dir="ltr"
/> />
<#if messagesPerField.existsError('totp')> <#if messagesPerField.existsError('totp')>
@ -78,7 +79,7 @@
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input type="text" class="${properties.kcInputClass!}" id="userLabel" name="userLabel" autocomplete="off" <input type="text" class="${properties.kcInputClass!}" id="userLabel" name="userLabel" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('userLabel')>true</#if>" aria-invalid="<#if messagesPerField.existsError('userLabel')>true</#if>" dir="ltr"
/> />
<#if messagesPerField.existsError('userLabel')> <#if messagesPerField.existsError('userLabel')>

View file

@ -10,7 +10,7 @@
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input id="device-user-code" name="device_user_code" autocomplete="off" type="text" class="${properties.kcInputClass!}" autofocus /> <input id="device-user-code" name="device_user_code" autocomplete="off" type="text" class="${properties.kcInputClass!}" autofocus dir="ltr" />
</div> </div>
</div> </div>

View file

@ -30,7 +30,8 @@
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input id="otp" name="otp" autocomplete="off" type="text" class="${properties.kcInputClass!}" <input id="otp" name="otp" autocomplete="off" type="text" class="${properties.kcInputClass!}"
autofocus aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"/> autofocus aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"
dir="ltr" />
<#if messagesPerField.existsError('totp')> <#if messagesPerField.existsError('totp')>
<span id="input-error-otp-code" class="${properties.kcInputErrorMessageClass!}" <span id="input-error-otp-code" class="${properties.kcInputErrorMessageClass!}"

View file

@ -79,7 +79,8 @@
class="${properties.kcInputClass!}" name="username" class="${properties.kcInputClass!}" name="username"
value="${(login.username!'')}" value="${(login.username!'')}"
autocomplete="username webauthn" autocomplete="username webauthn"
type="text" autofocus autocomplete="off"/> type="text" autofocus autocomplete="off"
dir="ltr"/>
<#if messagesPerField.existsError('username')> <#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc} ${kcSanitize(messagesPerField.get('username'))?no_esc}

View file

@ -10,7 +10,7 @@
<div class="${properties.kcFormGroupClass!} no-bottom-margin"> <div class="${properties.kcFormGroupClass!} no-bottom-margin">
<hr/> <hr/>
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" <input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password"
type="password" autocomplete="on" autofocus type="password" autocomplete="on" autofocus
aria-invalid="<#if messagesPerField.existsError('password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password')>true</#if>"

View file

@ -17,7 +17,8 @@
autocomplete="off" autocomplete="off"
type="text" type="text"
class="${properties.kcInputClass!}" class="${properties.kcInputClass!}"
autofocus/> autofocus
dir="ltr"/>
<#if messagesPerField.existsError('recoveryCodeInput')> <#if messagesPerField.existsError('recoveryCodeInput')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">

View file

@ -9,7 +9,7 @@
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label> <label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${(auth.attemptedUsername!'')}" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"/> <input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${(auth.attemptedUsername!'')}" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>" dir="ltr"/>
<#if messagesPerField.existsError('username')> <#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc} ${kcSanitize(messagesPerField.get('username'))?no_esc}

View file

@ -10,7 +10,7 @@
<label for="password-new" class="${properties.kcLabelClass!}">${msg("passwordNew")}</label> <label for="password-new" class="${properties.kcLabelClass!}">${msg("passwordNew")}</label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input type="password" id="password-new" name="password-new" class="${properties.kcInputClass!}" <input type="password" id="password-new" name="password-new" class="${properties.kcInputClass!}"
autofocus autocomplete="new-password" autofocus autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
@ -36,7 +36,7 @@
<label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> <label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input type="password" id="password-confirm" name="password-confirm" <input type="password" id="password-confirm" name="password-confirm"
class="${properties.kcInputClass!}" class="${properties.kcInputClass!}"
autocomplete="new-password" autocomplete="new-password"

View file

@ -17,7 +17,8 @@
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
class="${properties.kcInputClass!}" name="username" class="${properties.kcInputClass!}" name="username"
value="${(login.username!'')}" value="${(login.username!'')}"
type="text" autofocus autocomplete="off"/> type="text" autofocus autocomplete="off"
dir="ltr"/>
<#if messagesPerField.existsError('username')> <#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">

View file

@ -13,6 +13,7 @@
<input tabindex="2" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="username" <input tabindex="2" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="username"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
dir="ltr"
/> />
<#if messagesPerField.existsError('username','password')> <#if messagesPerField.existsError('username','password')>
@ -27,7 +28,7 @@
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input tabindex="3" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="current-password" <input tabindex="3" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="current-password"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/> />

View file

@ -20,7 +20,7 @@
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> * <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> *
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input type="password" id="password" class="${properties.kcInputClass!}" name="password" <input type="password" id="password" class="${properties.kcInputClass!}" name="password"
autocomplete="new-password" autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
@ -47,7 +47,7 @@
class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> * class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> *
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<input type="password" id="password-confirm" class="${properties.kcInputClass!}" <input type="password" id="password-confirm" class="${properties.kcInputClass!}"
name="password-confirm" name="password-confirm"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"

View file

@ -1,6 +1,6 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
<!DOCTYPE html> <!DOCTYPE html>
<html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}"</#if>> <html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}" dir="${(locale.rtl)?then('rtl','ltr')}"</#if>>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -60,6 +60,7 @@
<div class="${properties.kcInputClass!} <#if messagesPerField.existsError('totp')>pf-m-error</#if>"> <div class="${properties.kcInputClass!} <#if messagesPerField.existsError('totp')>pf-m-error</#if>">
<input type="text" required id="totp" name="totp" autocomplete="off" <input type="text" required id="totp" name="totp" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>" aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"
dir="ltr"
/> />
<#if messagesPerField.existsError('totp')> <#if messagesPerField.existsError('totp')>
@ -88,6 +89,7 @@
<div class="${properties.kcInputClass!}"> <div class="${properties.kcInputClass!}">
<input type="text" id="userLabel" name="userLabel" autocomplete="off" <input type="text" id="userLabel" name="userLabel" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('userLabel')>true</#if>" aria-invalid="<#if messagesPerField.existsError('userLabel')>true</#if>"
dir="ltr"
/> />
<#if messagesPerField.existsError('userLabel')> <#if messagesPerField.existsError('userLabel')>

View file

@ -12,7 +12,7 @@
</span> </span>
</label> </label>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}"> <span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-new" name="password-new" autofocus autocomplete="new-password" <input type="password" id="password-new" name="password-new" autofocus autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/> />
@ -39,7 +39,7 @@
</span> </span>
</label> </label>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}"> <span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-confirm" name="password-confirm" <input type="password" id="password-confirm" name="password-confirm"
autocomplete="new-password" autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"

View file

@ -18,6 +18,7 @@
<span class="${properties.kcInputClass!} ${messagesPerField.existsError('username','password')?then('pf-m-error', '')}"> <span class="${properties.kcInputClass!} ${messagesPerField.existsError('username','password')?then('pf-m-error', '')}">
<input tabindex="1" id="username" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off" <input tabindex="1" id="username" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
dir="ltr"
/> />
<#if messagesPerField.existsError('username','password')> <#if messagesPerField.existsError('username','password')>
<span class="pf-v5-c-form-control__utilities"> <span class="pf-v5-c-form-control__utilities">
@ -42,7 +43,7 @@
<span class="pf-v5-c-form__label-text">${msg("password")}</span> <span class="pf-v5-c-form__label-text">${msg("password")}</span>
</label> </label>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<span class="${properties.kcInputClass!}"> <span class="${properties.kcInputClass!}">
<input tabindex="2" id="password" name="password" type="password" autocomplete="off" <input tabindex="2" id="password" name="password" type="password" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"

View file

@ -23,7 +23,7 @@
</span> </span>
</label> </label>
<span class="${properties.kcInputGroup!}"> <span class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}"> <span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password" name="password" <input type="password" id="password" name="password"
autocomplete="new-password" autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
@ -53,7 +53,7 @@
</span> </span>
</label> </label>
</div> </div>
<div class="${properties.kcInputGroup!}"> <div class="${properties.kcInputGroup!}" dir="ltr">
<span class="${properties.kcInputClass!}"> <span class="${properties.kcInputClass!}">
<input type="password" id="password-confirm" <input type="password" id="password-confirm"
name="password-confirm" name="password-confirm"

View file

@ -1,6 +1,6 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
<!DOCTYPE html> <!DOCTYPE html>
<html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}"</#if>> <html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}" dir="${(locale.rtl)?then('rtl','ltr')}"</#if>>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">