Use default locale from realm an intermediate fallback

closes #40990

Signed-off-by: Christian Janker <christian.janker@gmx.at>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
Christian Ja 2026-01-01 15:23:33 +01:00 committed by GitHub
parent 35ee49b5d4
commit 374e45b883
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 118 additions and 35 deletions

View File

@ -59,6 +59,10 @@ Additionally, both `IdentityProviderModel` and `IdentityProviderRepresentation`
configuration like `isHideOnLogin` to be null in order to not include these in Identity Provider types that do
not need these configurations.
=== Realm default locale attempted for translation fallbacks
When localization for a realm is enabled and a translation for a message key is unavailable for the language the user selected, {project_name} now attempts to find a matching message key with the realm's default locale before defaulting to English.
=== SPIFFE Identity Provider configuration changed
The SPIFFE Identity Provider preview feature now uses the `trustDomain` configuration instead of `issuer`. This change

View File

@ -221,7 +221,7 @@ public class ThemeResource {
new KeySource((String) e.getKey(), (String) e.getValue(), Source.THEME)).collect(toSet());
Map<Locale, Properties> realmLocalizationMessages = LocaleUtil.getRealmLocalizationTexts(realm, locale);
for (Locale currentLocale = locale; currentLocale != null; currentLocale = LocaleUtil.getParentLocale(currentLocale)) {
for (Locale currentLocale = locale; currentLocale != null; currentLocale = LocaleUtil.getParentLocale(currentLocale, realm)) {
final List<KeySource> realmOverride = realmLocalizationMessages.get(currentLocale).entrySet().stream().map(e ->
new KeySource((String) e.getKey(), (String) e.getValue(), Source.REALM)).collect(toList());
resultSet.addAll(realmOverride);

View File

@ -62,11 +62,28 @@ public class LocaleUtil {
/**
* Returns the parent locale of the given {@code locale}. If the locale just contains a language (e.g. "de"),
* returns the fallback locale "en". For "en" no parent exists, {@code null} is returned.
*
*
* @param locale the locale
* @return the parent locale, may be {@code null}
* @deprecated use {@link LocaleUtil#getParentLocale(Locale, RealmModel)} instead.
*/
@Deprecated(since = "26.5", forRemoval = true)
public static Locale getParentLocale(Locale locale) {
return getParentLocale(locale, null);
}
/**
* Returns the parent locale of the given {@code locale}. If the locale just contains a language (e.g. "de"),
* returns the fallback default locale of the realm or if that does not exist "en".
* For "en" no parent exists, {@code null} is returned.
*
* @return the parent locale, may be {@code null}
*/
public static Locale getParentLocale(Locale locale, RealmModel realm) {
if (Locale.ENGLISH.equals(locale)) {
return null;
}
if (locale.getVariant() != null && !locale.getVariant().isEmpty()) {
return new Locale(locale.getLanguage(), locale.getCountry());
}
@ -75,11 +92,20 @@ public class LocaleUtil {
return new Locale(locale.getLanguage());
}
if (!Locale.ENGLISH.equals(locale)) {
if (realm != null
&& realm.isInternationalizationEnabled()
&& realm.getDefaultLocale() != null
&& Locale.forLanguageTag(realm.getDefaultLocale()).getLanguage().equals(locale.getLanguage())) {
return Locale.ENGLISH;
}
return null;
if (realm != null
&& realm.isInternationalizationEnabled()
&& realm.getDefaultLocale() != null) {
return Locale.forLanguageTag(realm.getDefaultLocale());
}
return Locale.ENGLISH;
}
/**
@ -90,10 +116,10 @@ public class LocaleUtil {
* @param locale the locale
* @return the applicable locales
*/
static List<Locale> getApplicableLocales(Locale locale) {
static List<Locale> getApplicableLocales(Locale locale, RealmModel realm) {
List<Locale> applicableLocales = new ArrayList<>();
for (Locale currentLocale = locale; currentLocale != null; currentLocale = getParentLocale(currentLocale)) {
for (Locale currentLocale = locale; currentLocale != null; currentLocale = getParentLocale(currentLocale, realm)) {
applicableLocales.add(currentLocale);
}
@ -107,10 +133,10 @@ public class LocaleUtil {
* @param locale the locale
* @param messages the (locale-)grouped messages
* @return the merged properties
* @see #mergeGroupedMessages(Locale, Map, Map)
* @see #mergeGroupedMessages(RealmModel, Locale, Map, Map)
*/
public static Properties mergeGroupedMessages(Locale locale, Map<Locale, Properties> messages) {
return mergeGroupedMessages(locale, messages, null);
public static Properties mergeGroupedMessages(RealmModel realm, Locale locale, Map<Locale, Properties> messages) {
return mergeGroupedMessages(realm, locale, messages, null);
}
/**
@ -147,11 +173,11 @@ public class LocaleUtil {
* @param secondMessages may be {@code null}, the second (locale-)grouped messages, having lower priority (per
* locale) than {@code firstMessages}
* @return the merged properties
* @see #mergeGroupedMessages(Locale, Map)
* @see #mergeGroupedMessages(RealmModel, Locale, Map)
*/
public static Properties mergeGroupedMessages(Locale locale, Map<Locale, Properties> firstMessages,
public static Properties mergeGroupedMessages(RealmModel realm, Locale locale, Map<Locale, Properties> firstMessages,
Map<Locale, Properties> secondMessages) {
List<Locale> applicableLocales = getApplicableLocales(locale);
List<Locale> applicableLocales = getApplicableLocales(locale, realm);
Properties mergedProperties = new Properties();
@ -186,7 +212,7 @@ public class LocaleUtil {
* the theme properties, but only when defined for the same locale. In general, texts for a more specific locale
* take precedence over texts for a less specific locale.
* <p>
* For implementation details, see {@link #mergeGroupedMessages(Locale, Map, Map)}.
* For implementation details, see {@link #mergeGroupedMessages(RealmModel, Locale, Map, Map)}.
*
* @param realm the realm from which the localization texts should be used
* @param locale the locale for which the relevant texts should be retrieved
@ -197,13 +223,13 @@ public class LocaleUtil {
Map<Locale, Properties> themeMessages) {
Map<Locale, Properties> realmLocalizationMessages = getRealmLocalizationTexts(realm, locale);
return mergeGroupedMessages(locale, realmLocalizationMessages, themeMessages);
return mergeGroupedMessages(realm, locale, realmLocalizationMessages, themeMessages);
}
public static Map<Locale, Properties> getRealmLocalizationTexts(RealmModel realm, Locale locale) {
LinkedHashMap<Locale, Properties> groupedMessages = new LinkedHashMap<>();
List<Locale> applicableLocales = getApplicableLocales(locale);
List<Locale> applicableLocales = getApplicableLocales(locale, realm);
for (Locale applicableLocale : applicableLocales) {
Map<String, String> currentRealmLocalizationTexts =
realm.getRealmLocalizationTextsByLocale(applicableLocale.toLanguageTag());

View File

@ -70,10 +70,11 @@ public class DefaultThemeManager implements ThemeManager {
public Theme getTheme(String name, Theme.Type type) {
Theme theme = factory.getCachedTheme(name, type);
if (theme == null) {
theme = loadTheme(name, type);
RealmModel realm = session.getContext().getRealm();
theme = loadTheme(name, type, realm);
if (theme == null) {
String defaultThemeName = session.getProvider(ThemeSelectorProvider.class).getDefaultThemeName(type);
theme = loadTheme(defaultThemeName, type);
theme = loadTheme(defaultThemeName, type, realm);
log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
} else {
theme = factory.addCachedTheme(name, type, theme);
@ -106,7 +107,7 @@ public class DefaultThemeManager implements ThemeManager {
public void close() {
}
private Theme loadTheme(String name, Theme.Type type) {
private Theme loadTheme(String name, Theme.Type type, RealmModel realm) {
Theme theme = findTheme(name, type);
if (theme == null) {
return null;
@ -131,7 +132,7 @@ public class DefaultThemeManager implements ThemeManager {
}
}
return new ExtendingTheme(themes, session.getAllProviders(ThemeResourceProvider.class));
return new ExtendingTheme(realm, themes, session.getAllProviders(ThemeResourceProvider.class));
}
private Theme findTheme(String name, Theme.Type type) {
@ -162,6 +163,7 @@ public class DefaultThemeManager implements ThemeManager {
private static class ExtendingTheme implements Theme {
private final RealmModel realm;
private final List<Theme> themes;
private final Set<ThemeResourceProvider> themeResourceProviders;
@ -172,7 +174,8 @@ public class DefaultThemeManager implements ThemeManager {
private Pattern compiledContentHashPattern;
public ExtendingTheme(List<Theme> themes, Set<ThemeResourceProvider> themeResourceProviders) {
public ExtendingTheme(RealmModel realm, List<Theme> themes, Set<ThemeResourceProvider> themeResourceProviders) {
this.realm = realm;
this.themes = themes;
this.themeResourceProviders = themeResourceProviders;
try {
@ -251,7 +254,7 @@ public class DefaultThemeManager implements ThemeManager {
@Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
Map<Locale, Properties> messagesByLocale = getMessagesByLocale(baseBundlename, locale);
return LocaleUtil.mergeGroupedMessages(locale, messagesByLocale);
return LocaleUtil.mergeGroupedMessages(realm, locale, messagesByLocale);
}
@Override
@ -267,7 +270,7 @@ public class DefaultThemeManager implements ThemeManager {
private Map<Locale, Properties> getMessagesByLocale(String baseBundlename, Locale locale) throws IOException {
if (messages.get(baseBundlename) == null || messages.get(baseBundlename).get(locale) == null) {
Locale parent = getParent(locale);
Locale parent = LocaleUtil.getParentLocale(locale, realm);
Map<Locale, Properties> parentMessages =
parent == null ? Collections.emptyMap() : getMessagesByLocale(baseBundlename, parent);
@ -376,9 +379,6 @@ public class DefaultThemeManager implements ThemeManager {
}
}
private static Locale getParent(Locale locale) {
return LocaleUtil.getParentLocale(locale);
}
private List<ThemeProvider> getProviders() {
if (providers == null) {

View File

@ -8,6 +8,9 @@ import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.RealmModelDelegate;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
@ -18,29 +21,79 @@ import static org.hamcrest.MatcherAssert.assertThat;
* @author <a href="mailto:daniel.fesenmeyer@bosch.com">Daniel Fesenmeyer</a>
*/
public class LocaleUtilTest {
RealmModel REALM_ITALIAN = new RealmModelDelegate(null) {
@Override
public boolean isInternationalizationEnabled() {
return true;
}
@Override
public String getDefaultLocale() {
return "it";
}
};
RealmModel REALM_ITALIAN_SWITZERLAND = new RealmModelDelegate(null) {
@Override
public boolean isInternationalizationEnabled() {
return true;
}
@Override
public String getDefaultLocale() {
return "it-CH";
}
};
RealmModel REALM_ENGLISH = new RealmModelDelegate(null) {
@Override
public boolean isInternationalizationEnabled() {
return true;
}
@Override
public String getDefaultLocale() {
return "en";
}
};
private static final Locale LOCALE_DE_CH = Locale.forLanguageTag("de-CH");
private static final Locale LOCALE_DE_CH_1996 = Locale.forLanguageTag("de-CH-1996");
@Test
public void getParentLocale() {
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH_1996), equalTo(LOCALE_DE_CH));
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH), equalTo(Locale.GERMAN));
assertThat(LocaleUtil.getParentLocale(Locale.GERMAN), equalTo(Locale.ENGLISH));
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH_1996, null), equalTo(LOCALE_DE_CH));
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH, null), equalTo(Locale.GERMAN));
assertThat(LocaleUtil.getParentLocale(Locale.GERMAN, null), equalTo(Locale.ENGLISH));
assertThat(LocaleUtil.getParentLocale(Locale.ENGLISH), nullValue());
assertThat(LocaleUtil.getParentLocale(Locale.ENGLISH, null), nullValue());
}
@Test
public void getParentLocaleWithRealmDefaultLocaleFallback() {
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH_1996, REALM_ITALIAN), equalTo(LOCALE_DE_CH));
assertThat(LocaleUtil.getParentLocale(LOCALE_DE_CH, REALM_ITALIAN), equalTo(Locale.GERMAN));
assertThat(LocaleUtil.getParentLocale(Locale.GERMAN, REALM_ITALIAN), equalTo(Locale.ITALIAN));
assertThat(LocaleUtil.getParentLocale(Locale.ITALIAN, REALM_ITALIAN), equalTo(Locale.ENGLISH));
assertThat(LocaleUtil.getParentLocale(Locale.ENGLISH, REALM_ITALIAN), nullValue());
}
@Test
public void getApplicableLocales() {
assertThat(LocaleUtil.getApplicableLocales(LOCALE_DE_CH_1996),
assertThat(LocaleUtil.getApplicableLocales(LOCALE_DE_CH_1996, null),
equalTo(Arrays.asList(LOCALE_DE_CH_1996, LOCALE_DE_CH, Locale.GERMAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(LOCALE_DE_CH),
assertThat(LocaleUtil.getApplicableLocales(LOCALE_DE_CH, null),
equalTo(Arrays.asList(LOCALE_DE_CH, Locale.GERMAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.GERMAN),
assertThat(LocaleUtil.getApplicableLocales(Locale.GERMAN, null),
equalTo(Arrays.asList(Locale.GERMAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.GERMAN, REALM_ITALIAN),
equalTo(Arrays.asList(Locale.GERMAN, Locale.ITALIAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.GERMAN, REALM_ITALIAN_SWITZERLAND),
equalTo(Arrays.asList(Locale.GERMAN, Locale.forLanguageTag("it-CH"), Locale.ITALIAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.GERMAN, REALM_ENGLISH),
equalTo(Arrays.asList(Locale.GERMAN, Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.ENGLISH), equalTo(Collections.singletonList(Locale.ENGLISH)));
assertThat(LocaleUtil.getApplicableLocales(Locale.ENGLISH, null), equalTo(Collections.singletonList(Locale.ENGLISH)));
}
@Test
@ -75,7 +128,7 @@ public class LocaleUtilTest {
keyDefinedForLanguageAndParents, keyDefinedForEnglishOnly), Locale.ENGLISH);
groupedMessages.put(Locale.ENGLISH, englishMessages);
Properties mergedMessages = LocaleUtil.mergeGroupedMessages(LOCALE_DE_CH_1996, groupedMessages);
Properties mergedMessages = LocaleUtil.mergeGroupedMessages(null, LOCALE_DE_CH_1996, groupedMessages);
Properties expectedMergedMessages = new Properties();
addTestValue(expectedMergedMessages, keyDefinedEverywhere, LOCALE_DE_CH_1996);
@ -164,7 +217,7 @@ public class LocaleUtilTest {
groupedMessages2.put(Locale.ENGLISH, english2Messages);
Properties mergedMessages =
LocaleUtil.mergeGroupedMessages(LOCALE_DE_CH_1996, groupedMessages1, groupedMessages2);
LocaleUtil.mergeGroupedMessages(null, LOCALE_DE_CH_1996, groupedMessages1, groupedMessages2);
Properties expectedMergedMessages = new Properties();
addTestValue(expectedMergedMessages, keyDefinedForVariantFromMessages1AndFallbacks, LOCALE_DE_CH_1996,