LocaleUtil.java
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.services.util;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.locale.LocaleUpdaterProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:daniel.fesenmeyer@bosch.com">Daniel Fesenmeyer</a>
*/
public class LocaleUtil {
private LocaleUtil() {
// noop
}
public static void processLocaleParam(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession) {
if (realm.isInternationalizationEnabled()) {
String locale = session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM);
if (locale != null) {
if (authSession != null) {
authSession.setAuthNote(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);
} else {
// Might be on info/error page when we don't have authenticationSession
session.setAttribute(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);
}
LocaleUpdaterProvider localeUpdater = session.getProvider(LocaleUpdaterProvider.class);
localeUpdater.updateLocaleCookie(locale);
}
}
}
/**
* 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}
*/
public static Locale getParentLocale(Locale locale) {
if (locale.getVariant() != null && !locale.getVariant().isEmpty()) {
return new Locale(locale.getLanguage(), locale.getCountry());
}
if (locale.getCountry() != null && !locale.getCountry().isEmpty()) {
return new Locale(locale.getLanguage());
}
if (!Locale.ENGLISH.equals(locale)) {
return Locale.ENGLISH;
}
return null;
}
/**
* Gets the applicable locales for the given locale.
* <p>
* Example: Locale "de-CH" has the applicable locales "de-CH", "de" and "en" (in exactly that order).
*
* @param locale the locale
* @return the applicable locales
*/
static List<Locale> getApplicableLocales(Locale locale) {
List<Locale> applicableLocales = new ArrayList<>();
for (Locale currentLocale = locale; currentLocale != null; currentLocale = getParentLocale(currentLocale)) {
applicableLocales.add(currentLocale);
}
return applicableLocales;
}
/**
* Merge the given (locale-)grouped messages into one instance of {@link Properties}, applicable for the given
* {@code locale}.
*
* @param locale the locale
* @param messages the (locale-)grouped messages
* @return the merged properties
* @see #mergeGroupedMessages(Locale, Map, Map)
*/
public static Properties mergeGroupedMessages(Locale locale, Map<Locale, Properties> messages) {
return mergeGroupedMessages(locale, messages, null);
}
/**
* Merge the given (locale-)grouped messages into one instance of {@link Properties}, applicable for the given
* {@code locale}.
* <p>
* The priority of the messages is as follows (abbreviations: F = firstMessages, S = secondMessages):
* <ol>
* <li>F <language-region-variant></li>
* <li>S <language-region-variant></li>
* <li>F <language-region></li>
* <li>S <language-region></li>
* <li>F <language></li>
* <li>S <language></li>
* <li>F en</li>
* <li>S en</li>
* </ol>
* <p>
* Example for the message priority for locale "de-CH-1996" (language "de", region "CH", variant "1996):
* <ol>
* <li>F de-CH-1996</li>
* <li>S de-CH-1996</li>
* <li>F de-CH</li>
* <li>S de-CH</li>
* <li>F de</li>
* <li>S de</li>
* <li>F en</li>
* <li>S en</li>
* </ol>
*
* @param locale the locale
* @param firstMessages the first (locale-)grouped messages, having higher priority (per locale) than
* {@code secondMessages}
* @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)
*/
public static Properties mergeGroupedMessages(Locale locale, Map<Locale, Properties> firstMessages,
Map<Locale, Properties> secondMessages) {
List<Locale> applicableLocales = getApplicableLocales(locale);
Properties mergedProperties = new Properties();
/*
* iterate starting from the end of the list in order to add the least relevant messages first (in order to be
* overwritten by more relevant messages)
*/
ListIterator<Locale> itr = applicableLocales.listIterator(applicableLocales.size());
while (itr.hasPrevious()) {
Locale currentLocale = itr.previous();
// add secondMessages first, if specified (to be overwritten by firstMessages)
if (secondMessages != null) {
Properties currentLocaleSecondMessages = secondMessages.get(currentLocale);
if (currentLocaleSecondMessages != null) {
mergedProperties.putAll(currentLocaleSecondMessages);
}
}
// add firstMessages, overwriting secondMessages (if specified)
Properties currentLocaleFirstMessages = firstMessages.get(currentLocale);
if (currentLocaleFirstMessages != null) {
mergedProperties.putAll(currentLocaleFirstMessages);
}
}
return mergedProperties;
}
/**
* Enhance the properties from a theme with realm localization texts. Realm localization texts take precedence over
* 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)}.
*
* @param realm the realm from which the localization texts should be used
* @param locale the locale for which the relevant texts should be retrieved
* @param themeMessages the theme messages, which should be enhanced and maybe overwritten
* @return the enhanced properties
*/
public static Properties enhancePropertiesWithRealmLocalizationTexts(RealmModel realm, Locale locale,
Map<Locale, Properties> themeMessages) {
Map<Locale, Properties> realmLocalizationMessages = getRealmLocalizationTexts(realm, locale);
return mergeGroupedMessages(locale, realmLocalizationMessages, themeMessages);
}
private static Map<Locale, Properties> getRealmLocalizationTexts(RealmModel realm, Locale locale) {
LinkedHashMap<Locale, Properties> groupedMessages = new LinkedHashMap<>();
List<Locale> applicableLocales = getApplicableLocales(locale);
for (Locale applicableLocale : applicableLocales) {
Map<String, String> currentRealmLocalizationTexts =
realm.getRealmLocalizationTextsByLocale(applicableLocale.toLanguageTag());
Properties currentMessages = new Properties();
currentMessages.putAll(currentRealmLocalizationTexts);
groupedMessages.put(applicableLocale, currentMessages);
}
return groupedMessages;
}
}