com.helger.commons.locale.LocaleHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-commons Show documentation
Show all versions of ph-commons Show documentation
Java 1.8+ Library with tons of utility classes required in all projects
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* 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 com.helger.commons.locale;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import com.helger.commons.CGlobal;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.CodingStyleguideUnaware;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsImmutableObject;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.cache.Cache;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.regex.RegExHelper;
import com.helger.commons.state.EChange;
import com.helger.commons.string.StringHelper;
import com.helger.commons.system.SystemHelper;
/**
* Misc locale utility methods.
*
* @author Philip Helger
*/
@Immutable
public final class LocaleHelper
{
/**
* Small cache for the resolved locales.
*
* @author Philip Helger
*/
@ThreadSafe
private static final class LocaleListCache extends Cache >
{
public LocaleListCache ()
{
// Unlimited size
super (aBaseLocale -> {
ValueEnforcer.notNull (aBaseLocale, "BaseLocale");
// List has a maximum of 3 entries
final ICommonsList ret = new CommonsArrayList <> (3);
final String sLanguage = aBaseLocale.getLanguage ();
if (sLanguage.length () > 0)
{
final LocaleCache aLC = LocaleCache.getInstance ();
// Use only the language
ret.add (0, aLC.getLocale (sLanguage));
final String sCountry = aBaseLocale.getCountry ();
if (sCountry.length () > 0)
{
// Use language + country
ret.add (0, aLC.getLocale (sLanguage, sCountry));
final String sVariant = aBaseLocale.getVariant ();
if (sVariant.length () > 0)
{
// Use language + country + variant
ret.add (0, aLC.getLocale (sLanguage, sCountry, sVariant));
}
}
}
return ret.getAsUnmodifiable ();
}, CGlobal.ILLEGAL_UINT, LocaleListCache.class.getName ());
}
}
/**
* Separates language and country in a locale string representation.
*/
public static final char LOCALE_SEPARATOR = '_';
/**
* The language string representing the "all" locale. See {@link #LOCALE_ALL}.
*/
public static final String STR_ALL = "all";
/**
* the default locale which means "all locales".
*/
public static final Locale LOCALE_ALL = new Locale (STR_ALL, "", "");
/**
* The language string representing the "independent" locale. See
* {@link #LOCALE_INDEPENDENT}.
*/
public static final String STR_INDEPENDENT = "independent";
/**
* the default locale which means "locale independent".
*/
public static final Locale LOCALE_INDEPENDENT = new Locale (STR_INDEPENDENT, "", "");
private static final String LOCALE_ALL_STR = LOCALE_ALL.toString ();
private static final String LOCALE_INDEPENDENT_STR = LOCALE_INDEPENDENT.toString ();
private static final LocaleListCache LOCALE_LIST_CACHE = new LocaleListCache ();
private static final ICommonsMap COUNTRY_ISO3TO2 = new CommonsHashMap <> ();
@PresentForCodeCoverage
private static final LocaleHelper INSTANCE = new LocaleHelper ();
static
{
for (final String sISO : Locale.getISOCountries ())
{
final Locale aLocale = new Locale ("", sISO);
COUNTRY_ISO3TO2.put (aLocale.getISO3Country (), aLocale.getCountry ());
}
}
private LocaleHelper ()
{}
/**
* Get the display name of the passed language in the currently selected UI
* language.
*
* @param aLocale
* The locale from which the display name is required. May be
* null
.
* @param aContentLocale
* The locale in which the name of the locale is required. If aLocale
* is "de" and display locale is "de" the result would be "Deutsch"
* whereas if display locale is "en" the result would be "German".
* @return the display name of the language or a fixed text if the passed
* Locale is null
, "all" or "independent"
* @see #LOCALE_ALL
* @see #LOCALE_INDEPENDENT
*/
@Nonnull
public static String getLocaleDisplayName (@Nullable final Locale aLocale, @Nonnull final Locale aContentLocale)
{
ValueEnforcer.notNull (aContentLocale, "ContentLocale");
if (aLocale == null || aLocale.equals (LOCALE_INDEPENDENT))
return ELocaleName.ID_LANGUAGE_INDEPENDENT.getDisplayText (aContentLocale);
if (aLocale.equals (LOCALE_ALL))
return ELocaleName.ID_LANGUAGE_ALL.getDisplayText (aContentLocale);
return aLocale.getDisplayName (aContentLocale);
}
/**
* Get the display name of the passed locale in the passed locale.
*
* @param aLocale
* The locale to use. May not be null
.
* @return The native display name of the passed locale.
*/
@Nonnull
public static String getLocaleNativeDisplayName (@Nonnull final Locale aLocale)
{
ValueEnforcer.notNull (aLocale, "Locale");
return getLocaleDisplayName (aLocale, aLocale);
}
/**
* Get all possible locale names in the passed locale
*
* @param aContentLocale
* the locale ID in which the language list is required
* @return The mapping from the input locale to the display text. The result
* map is not ordered.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsMap getAllLocaleDisplayNames (@Nonnull final Locale aContentLocale)
{
ValueEnforcer.notNull (aContentLocale, "ContentLocale");
return new CommonsHashMap <> (LocaleCache.getInstance ().getAllLocales (),
Function.identity (),
aLocale -> getLocaleDisplayName (aLocale, aContentLocale));
}
/**
* Get a list with all valid locale permutations of the passed locale. If the
* passed locale has no language, always an empty list is returned.
* Examples:
*
* - "de_AT" => ["de_AT", "de"]
* - "en_US" => ["en_US", "en"]
* - "de" => ["de"]
* - "de_DE_Variant" => ["de_DE_Variant", "de_DE", "de"]
* - "" => []
* - "_AT" => []
* - "_AT_Variant" => []
*
*
* @param aLocale
* The locale to get the permutation from. May not be null
* .
* @return A non-null
unmodifiable list of all permutations of
* the locale, with the most specific locale being first. The returned
* list has never more than three entries. The returned list may have
* no entries, if the passed locale has no language.
*/
@Nonnull
@ReturnsImmutableObject
@CodingStyleguideUnaware
public static List getCalculatedLocaleListForResolving (@Nonnull final Locale aLocale)
{
ValueEnforcer.notNull (aLocale, "Locale");
return LOCALE_LIST_CACHE.getFromCache (aLocale);
}
/**
* Convert a String in the form "language-country-variant" to a Locale object.
* Language needs to have exactly 2 characters. Country is optional but if
* present needs to have exactly 2 characters. Variant is optional.
*
* @param sLocaleAsString
* The string representation to be converted to a Locale.
* @return Never null
. If the passed parameter is
* null
or empty, {@link SystemHelper#getSystemLocale()}
* is returned.
*/
@Nonnull
public static Locale getLocaleFromString (@Nullable final String sLocaleAsString)
{
if (StringHelper.hasNoText (sLocaleAsString))
{
// not specified => getDefault
return SystemHelper.getSystemLocale ();
}
String sLanguage;
String sCountry;
String sVariant;
int i1 = sLocaleAsString.indexOf (LOCALE_SEPARATOR);
if (i1 < 0)
{
// No separator present -> use as is
sLanguage = sLocaleAsString;
sCountry = "";
sVariant = "";
}
else
{
// Language found
sLanguage = sLocaleAsString.substring (0, i1);
++i1;
// Find next separator
final int i2 = sLocaleAsString.indexOf (LOCALE_SEPARATOR, i1);
if (i2 < 0)
{
// No other separator -> country is the rest
sCountry = sLocaleAsString.substring (i1);
sVariant = "";
}
else
{
// We have country and variant
sCountry = sLocaleAsString.substring (i1, i2);
sVariant = sLocaleAsString.substring (i2 + 1);
}
}
// Unify elements
if (sLanguage.length () == 2)
sLanguage = sLanguage.toLowerCase (Locale.US);
else
sLanguage = "";
if (sCountry.length () == 2)
sCountry = sCountry.toUpperCase (Locale.US);
else
sCountry = "";
if (sVariant.length () > 0 && (sLanguage.length () == 2 || sCountry.length () == 2))
sVariant = sVariant.toUpperCase (Locale.US);
else
sVariant = "";
// And now resolve using the locale cache
return LocaleCache.getInstance ().getLocale (sLanguage, sCountry, sVariant);
}
@Nullable
public static Locale getLocaleToUseOrNull (@Nonnull final Locale aRequestLocale, @Nonnull final Collection aAvailableLocales)
{
return getLocaleToUseOrFallback (aRequestLocale, aAvailableLocales, null);
}
@Nullable
public static Locale getLocaleToUseOrFallback (@Nonnull final Locale aRequestLocale,
@Nonnull final Collection aAvailableLocales,
@Nullable final Locale aFallback)
{
ValueEnforcer.notNull (aRequestLocale, "RequestLocale");
ValueEnforcer.notNull (aAvailableLocales, "AvailableLocales");
// first check direct match
if (aAvailableLocales.contains (aRequestLocale))
return aRequestLocale;
// try to resolve a more general locale than asked for
for (final Locale aCurrentLocale : getCalculatedLocaleListForResolving (aRequestLocale))
if (aAvailableLocales.contains (aCurrentLocale))
return aCurrentLocale;
// try to resolve a more specific locale than asked for (only check
// language)
final String sRequestLanguage = aRequestLocale.getLanguage ();
if (sRequestLanguage != null)
for (final Locale aCurrentAvailableLocale : aAvailableLocales)
if (sRequestLanguage.equals (aCurrentAvailableLocale.getLanguage ()))
return aCurrentAvailableLocale;
// If none matched, check if "all" is provided
if (aAvailableLocales.contains (LOCALE_ALL))
return LOCALE_ALL;
// If none matched, check if "independent" is provided
if (aAvailableLocales.contains (LOCALE_INDEPENDENT))
return LOCALE_INDEPENDENT;
// No matching found -> fallback locale
return aFallback;
}
/**
* Check if the passed locale is one of the special locales "all" or
* "independent"
*
* @param aLocale
* The locale to check. May be null
.
* @return if the passed locale is not null
and a special locale.
* @see #LOCALE_ALL
* @see #LOCALE_INDEPENDENT
*/
public static boolean isSpecialLocale (@Nullable final Locale aLocale)
{
return LOCALE_ALL.equals (aLocale) || LOCALE_INDEPENDENT.equals (aLocale);
}
/**
* Check if the passed locale is one of the special locales "all" or
* "independent"
*
* @param sLocale
* The locale to check. May be null
.
* @return if the passed locale is not null
and a special locale.
* @see #LOCALE_ALL
* @see #LOCALE_INDEPENDENT
*/
public static boolean isSpecialLocaleCode (@Nullable final String sLocale)
{
return LOCALE_ALL_STR.equalsIgnoreCase (sLocale) || LOCALE_INDEPENDENT_STR.equalsIgnoreCase (sLocale);
}
@Nullable
public static String getValidLanguageCode (@Nullable final String sCode)
{
if (StringHelper.hasText (sCode) && (RegExHelper.stringMatchesPattern ("[a-zA-Z]{2,8}", sCode) || isSpecialLocaleCode (sCode)))
{
return sCode.toLowerCase (Locale.ROOT);
}
return null;
}
/**
* Ensure a country code is valid by converting it to a 2-character uppercase
* code. This method also maps 3 letter codes to 2 letter codes.
*
* @param sCode
* The country code to check. May be null
.
* @return null
if the source code is not a support country code.
*/
@Nullable
public static String getValidCountryCode (@Nullable final String sCode)
{
// Allow for 2 or 3 letter codes ("AT" and "AUT")
if (StringHelper.hasText (sCode))
{
// Allow for 2 letter codes ("AT")
// Second version allows for "1A" as Kosovo code
if (RegExHelper.stringMatchesPattern ("[a-zA-Z]{2}|[0-9][a-zA-Z]|[0-9]{3}", sCode))
{
return sCode.toUpperCase (Locale.ROOT);
}
// Allow for 3 letter codes ("AUT")
if (RegExHelper.stringMatchesPattern ("[a-zA-Z]{3}", sCode))
{
final String sAlpha3 = sCode.toUpperCase (Locale.ROOT);
final String sAlpha2 = COUNTRY_ISO3TO2.get (sAlpha3);
return sAlpha2 != null ? sAlpha2 : sAlpha3;
}
}
return null;
}
/**
* Clear all stored locale lists
*
* @return {@link EChange}.
*/
@Nonnull
public static EChange clearCache ()
{
return LOCALE_LIST_CACHE.clearCache ();
}
}