/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.portal.language;

import com.liferay.petra.string.CharPool;
import com.liferay.portal.kernel.cache.PortalCache;
import com.liferay.portal.kernel.cache.PortalCacheHelperUtil;
import com.liferay.portal.kernel.cache.PortalCacheManagerNames;
import com.liferay.portal.kernel.cache.PortalCacheMapSynchronizeUtil;
import com.liferay.portal.kernel.cache.PortalCacheMapSynchronizeUtil.Synchronizer;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.language.Language;
import com.liferay.portal.kernel.language.LanguageWrapper;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.CompanyConstants;
import com.liferay.portal.kernel.model.Group;
import com.liferay.portal.kernel.model.GroupConstants;
import com.liferay.portal.kernel.security.auth.CompanyThreadLocal;
import com.liferay.portal.kernel.security.pacl.DoPrivileged;
import com.liferay.portal.kernel.service.GroupLocalServiceUtil;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.CookieKeys;
import com.liferay.portal.kernel.util.FastDateFormatConstants;
import com.liferay.portal.kernel.util.FastDateFormatFactoryUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HtmlUtil;
import com.liferay.portal.kernel.util.JavaConstants;
import com.liferay.portal.kernel.util.LocaleUtil;
import com.liferay.portal.kernel.util.ObjectValuePair;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.PortalUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.ResourceBundleLoader;
import com.liferay.portal.kernel.util.ResourceBundleUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Time;
import com.liferay.portal.kernel.util.UnicodeProperties;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.util.WebKeys;
import com.liferay.portal.util.PrefsPropsUtil;
import com.liferay.portal.util.PropsValues;

import java.io.Serializable;

import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.portlet.PortletConfig;
import javax.portlet.PortletRequest;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Provides various translation related functionalities for language keys
 * specified in portlet configurations and portal resource bundles.
 *
 * <p>
 * You can disable translations by setting the
 * <code>translations.disabled</code> property to <code>true</code> in
 * <code>portal.properties</code>.
 * </p>
 *
 * <p>
 * Depending on the context passed into these methods, the lookup might be
 * limited to the portal's resource bundle (e.g. when only a locale is passed),
 * or extended to include an individual portlet's resource bundle (e.g. when a
 * request object is passed). A portlet's resource bundle overrides the portal's
 * resources when both are present.
 * </p>
 *
 * @author Brian Wing Shun Chan
 * @author Andrius Vitkauskas
 * @author Eduardo Lundgren
 */
@DoPrivileged
public class LanguageImpl implements Language, Serializable {

	public void afterPropertiesSet() {
		_companyLocalesPortalCache = PortalCacheHelperUtil.getPortalCache(
			PortalCacheManagerNames.MULTI_VM,
			_COMPANY_LOCALES_PORTAL_CACHE_NAME);

		PortalCacheMapSynchronizeUtil.synchronize(
			_companyLocalesPortalCache, _companyLocalesBags,
			_removeSynchronizer);

		_groupLocalesPortalCache = PortalCacheHelperUtil.getPortalCache(
			PortalCacheManagerNames.MULTI_VM, _GROUP_LOCALES_PORTAL_CACHE_NAME);

		PortalCacheMapSynchronizeUtil.synchronize(
			_groupLocalesPortalCache, _groupLanguageCodeLocalesMapMap,
			_removeSynchronizer);

		PortalCacheMapSynchronizeUtil.synchronize(
			_groupLocalesPortalCache, _groupLanguageIdLocalesMap,
			_removeSynchronizer);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the single argument to be substituted into the pattern
	 *         and translated, if possible
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, LanguageWrapper argument) {

		return format(request, pattern, new LanguageWrapper[] {argument}, true);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the single argument to be substituted into the pattern
	 *         and translated, if possible
	 * @param  translateArguments whether the argument is translated
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, LanguageWrapper argument,
		boolean translateArguments) {

		return format(
			request, pattern, new LanguageWrapper[] {argument},
			translateArguments);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern and
	 *         translated, if possible
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern,
		LanguageWrapper[] arguments) {

		return format(request, pattern, arguments, true);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @param  translateArguments whether the arguments are translated
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, LanguageWrapper[] arguments,
		boolean translateArguments) {

		if (PropsValues.TRANSLATIONS_DISABLED) {
			return pattern;
		}

		String value = null;

		try {
			pattern = get(request, pattern);

			if (ArrayUtil.isNotEmpty(arguments)) {
				Object[] formattedArguments = new Object[arguments.length];

				for (int i = 0; i < arguments.length; i++) {
					if (translateArguments) {
						formattedArguments[i] =
							arguments[i].getBefore() +
								get(request, arguments[i].getText()) +
									arguments[i].getAfter();
					}
					else {
						formattedArguments[i] =
							arguments[i].getBefore() + arguments[i].getText() +
								arguments[i].getAfter();
					}
				}

				value = _decorateMessageFormat(
					request, pattern, formattedArguments);
			}
			else {
				value = pattern;
			}
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the single argument to be substituted into the pattern
	 *         and translated, if possible
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, Object argument) {

		return format(request, pattern, new Object[] {argument}, true);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the single argument to be substituted into the pattern
	 *         and translated, if possible
	 * @param  translateArguments whether the argument is translated
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, Object argument,
		boolean translateArguments) {

		return format(
			request, pattern, new Object[] {argument}, translateArguments);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern and
	 *         translated, if possible
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, Object[] arguments) {

		return format(request, pattern, arguments, true);
	}

	/**
	 * Returns the translated pattern using the current request's locale or, if
	 * the current request locale is not available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portlet configuration first, and if it's not
	 * found, it is done on the portal's resource bundle. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @param  translateArguments whether the arguments are translated
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		HttpServletRequest request, String pattern, Object[] arguments,
		boolean translateArguments) {

		if (PropsValues.TRANSLATIONS_DISABLED) {
			return pattern;
		}

		String value = null;

		try {
			pattern = get(request, pattern);

			if (ArrayUtil.isNotEmpty(arguments)) {
				for (int i = 0; i < arguments.length; i++) {
					if (translateArguments) {
						arguments[i] = get(request, arguments[i].toString());
					}
				}

				value = _decorateMessageFormat(request, pattern, arguments);
			}
			else {
				value = pattern;
			}
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns the translated pattern using the locale or, if the locale is not
	 * available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portal's resource bundle. If a translation for
	 * a given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  locale the locale to translate to
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		Locale locale, String pattern, List<Object> arguments) {

		return format(locale, pattern, arguments.toArray(), true);
	}

	/**
	 * Returns the translated pattern using the locale or, if the locale is not
	 * available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portal's resource bundle. If a translation for
	 * a given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  locale the locale to translate to
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the argument to be substituted into the pattern
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(Locale locale, String pattern, Object argument) {
		return format(locale, pattern, new Object[] {argument}, true);
	}

	/**
	 * Returns the translated pattern using the locale or, if the locale is not
	 * available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portal's resource bundle. If a translation for
	 * a given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  locale the locale to translate to
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  argument the argument to be substituted into the pattern
	 * @param  translateArguments whether the argument is translated
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		Locale locale, String pattern, Object argument,
		boolean translateArguments) {

		return format(
			locale, pattern, new Object[] {argument}, translateArguments);
	}

	/**
	 * Returns the translated pattern using the locale or, if the locale is not
	 * available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portal's resource bundle. If a translation for
	 * a given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  locale the locale to translate to
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(Locale locale, String pattern, Object[] arguments) {
		return format(locale, pattern, arguments, true);
	}

	/**
	 * Returns the translated pattern using the locale or, if the locale is not
	 * available, the server's default locale.
	 *
	 * <p>
	 * The lookup is done on the portal's resource bundle. If a translation for
	 * a given key does not exist, this method returns the requested key as the
	 * translation.
	 * </p>
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  locale the locale to translate to
	 * @param  pattern the key to look up in the current locale's resource file.
	 *         The key follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @param  translateArguments whether the arguments are translated
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholders
	 */
	@Override
	public String format(
		Locale locale, String pattern, Object[] arguments,
		boolean translateArguments) {

		if (PropsValues.TRANSLATIONS_DISABLED) {
			return pattern;
		}

		String value = null;

		try {
			pattern = get(locale, pattern);

			if (ArrayUtil.isNotEmpty(arguments)) {
				for (int i = 0; i < arguments.length; i++) {
					if (translateArguments) {
						arguments[i] = get(locale, arguments[i].toString());
					}
				}

				value = _decorateMessageFormat(locale, pattern, arguments);
			}
			else {
				value = pattern;
			}
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns the translated pattern in the resource bundle or, if the resource
	 * bundle is not available, the untranslated key. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  pattern the key to look up in the resource bundle. The key
	 *         follows the standard Java resource specification.
	 * @param  argument the argument to be substituted into the pattern
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		ResourceBundle resourceBundle, String pattern, Object argument) {

		return format(resourceBundle, pattern, new Object[] {argument}, true);
	}

	/**
	 * Returns the translated pattern in the resource bundle or, if the resource
	 * bundle is not available, the untranslated key. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 *
	 * <p>
	 * The substitute placeholder (e.g. <code>{0}</code>) is replaced with the
	 * argument, following the standard Java {@link ResourceBundle} notion of
	 * index based substitution.
	 * </p>
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  pattern the key to look up in the resource bundle. The key
	 *         follows the standard Java resource specification.
	 * @param  argument the argument to be substituted into the pattern
	 * @param  translateArguments whether the argument is translated
	 * @return the translated pattern, with the argument substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		ResourceBundle resourceBundle, String pattern, Object argument,
		boolean translateArguments) {

		return format(
			resourceBundle, pattern, new Object[] {argument},
			translateArguments);
	}

	/**
	 * Returns the translated pattern in the resource bundle or, if the resource
	 * bundle is not available, the untranslated key. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  pattern the key to look up in the resource bundle. The key
	 *         follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		ResourceBundle resourceBundle, String pattern, Object[] arguments) {

		return format(resourceBundle, pattern, arguments, true);
	}

	/**
	 * Returns the translated pattern in the resource bundle or, if the resource
	 * bundle is not available, the untranslated key. If a translation for a
	 * given key does not exist, this method returns the requested key as the
	 * translation.
	 *
	 * <p>
	 * The substitute placeholders (e.g. <code>{0}</code>, <code>{1}</code>,
	 * <code>{2}</code>, etc.) are replaced with the arguments, following the
	 * standard Java {@link ResourceBundle} notion of index based substitution.
	 * </p>
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  pattern the key to look up in the resource bundle. The key
	 *         follows the standard Java resource specification.
	 * @param  arguments the arguments to be substituted into the pattern
	 * @param  translateArguments whether the arguments are translated
	 * @return the translated pattern, with the arguments substituted in for the
	 *         pattern's placeholder
	 */
	@Override
	public String format(
		ResourceBundle resourceBundle, String pattern, Object[] arguments,
		boolean translateArguments) {

		if (PropsValues.TRANSLATIONS_DISABLED) {
			return pattern;
		}

		String value = null;

		try {
			pattern = get(resourceBundle, pattern);

			if (ArrayUtil.isNotEmpty(arguments)) {
				for (int i = 0; i < arguments.length; i++) {
					if (translateArguments) {
						arguments[i] = get(
							resourceBundle, arguments[i].toString());
					}
				}

				value = _decorateMessageFormat(
					resourceBundle.getLocale(), pattern, arguments);
			}
			else {
				value = pattern;
			}
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns the key's translation from the portlet configuration, or from the
	 * portal's resource bundle if the portlet configuration is unavailable.
	 *
	 * @param  request the request used to determine the key's context and
	 *         locale
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  key the translation key
	 * @return the key's translation, or the key if the translation is
	 *         unavailable
	 */
	@Override
	public String get(
		HttpServletRequest request, ResourceBundle resourceBundle, String key) {

		return get(request, resourceBundle, key, key);
	}

	@Override
	public String get(
		HttpServletRequest request, ResourceBundle resourceBundle, String key,
		String defaultValue) {

		String value = _get(resourceBundle, key);

		if (value != null) {
			return value;
		}

		return get(request, key, defaultValue);
	}

	@Override
	public String get(HttpServletRequest request, String key) {
		return get(request, key, key);
	}

	/**
	 * Returns the key's translation from the portlet configuration, or from the
	 * portal's resource bundle if the portlet configuration is unavailable.
	 *
	 * @param  request the request used to determine the key's context and
	 *         locale
	 * @param  key the translation key
	 * @param  defaultValue the value to return if there is no matching
	 *         translation
	 * @return the key's translation, or the default value if the translation is
	 *         unavailable
	 */
	@Override
	public String get(
		HttpServletRequest request, String key, String defaultValue) {

		if ((request == null) || (key == null)) {
			return defaultValue;
		}

		PortletConfig portletConfig = (PortletConfig)request.getAttribute(
			JavaConstants.JAVAX_PORTLET_CONFIG);

		Locale locale = _getLocale(request);

		if (portletConfig == null) {
			return get(locale, key, defaultValue);
		}

		ResourceBundle resourceBundle = portletConfig.getResourceBundle(locale);

		if (resourceBundle.containsKey(key)) {
			return _get(resourceBundle, key);
		}

		return get(locale, key, defaultValue);
	}

	/**
	 * Returns the key's translation from the portal's resource bundle.
	 *
	 * @param  locale the key's locale
	 * @param  key the translation key
	 * @return the key's translation
	 */
	@Override
	public String get(Locale locale, String key) {
		return get(locale, key, key);
	}

	/**
	 * Returns the key's translation from the portal's resource bundle.
	 *
	 * @param  locale the key's locale
	 * @param  key the translation key
	 * @param  defaultValue the value to return if there is no matching
	 *         translation
	 * @return the key's translation, or the default value if the translation is
	 *         unavailable
	 */
	@Override
	public String get(Locale locale, String key, String defaultValue) {
		if (PropsValues.TRANSLATIONS_DISABLED) {
			return key;
		}

		if ((locale == null) || (key == null)) {
			return defaultValue;
		}

		String value = LanguageResources.getMessage(locale, key);

		if (value != null) {
			return LanguageResources.fixValue(value);
		}

		if ((value == null) && (key.length() > 0) &&
			(key.charAt(key.length() - 1) == CharPool.CLOSE_BRACKET)) {

			int pos = key.lastIndexOf(CharPool.OPEN_BRACKET);

			if (pos != -1) {
				key = key.substring(0, pos);

				return get(locale, key, defaultValue);
			}
		}

		return defaultValue;
	}

	/**
	 * Returns the key's translation from the resource bundle.
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  key the translation key
	 * @return the key's translation
	 */
	@Override
	public String get(ResourceBundle resourceBundle, String key) {
		return get(resourceBundle, key, key);
	}

	/**
	 * Returns the key's translation from the resource bundle.
	 *
	 * @param  resourceBundle the requested key's resource bundle
	 * @param  key the translation key
	 * @param  defaultValue the value to return if there is no matching
	 *         translation
	 * @return the key's translation, or the default value if the translation is
	 *         unavailable
	 */
	@Override
	public String get(
		ResourceBundle resourceBundle, String key, String defaultValue) {

		String value = _get(resourceBundle, key);

		if (value != null) {
			return value;
		}

		return defaultValue;
	}

	/**
	 * Returns the locales configured for the portal. Locales can be configured
	 * in <code>portal.properties</code> using the <code>locales</code> and
	 * <code>locales.enabled</code> keys.
	 *
	 * @return the locales configured for the portal
	 */
	@Override
	public Set<Locale> getAvailableLocales() {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.getAvailableLocales();
	}

	@Override
	public Set<Locale> getAvailableLocales(long groupId) {
		if (groupId <= 0) {
			return getAvailableLocales();
		}

		try {
			if (isInheritLocales(groupId)) {
				Group group = GroupLocalServiceUtil.getGroup(groupId);

				CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag(
					group.getCompanyId());

				return companyLocalesBag.getAvailableLocales();
			}
		}
		catch (Exception e) {
		}

		Map<String, Locale> groupLanguageIdLocalesMap =
			_getGroupLanguageIdLocalesMap(groupId);

		return new LinkedHashSet<>(groupLanguageIdLocalesMap.values());
	}

	@Override
	public String getBCP47LanguageId(HttpServletRequest request) {
		Locale locale = PortalUtil.getLocale(request);

		return getBCP47LanguageId(locale);
	}

	@Override
	public String getBCP47LanguageId(Locale locale) {
		return LocaleUtil.toBCP47LanguageId(locale);
	}

	@Override
	public String getBCP47LanguageId(PortletRequest portletRequest) {
		Locale locale = PortalUtil.getLocale(portletRequest);

		return getBCP47LanguageId(locale);
	}

	@Override
	public Set<Locale> getCompanyAvailableLocales(long companyId) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag(companyId);

		return companyLocalesBag.getAvailableLocales();
	}

	/**
	 * Returns the language ID that the request is served with. The language ID
	 * is returned as a language code (e.g. <code>en</code>) or a specific
	 * variant (e.g. <code>en_GB</code>).
	 *
	 * @param  request the request used to determine the language ID
	 * @return the language ID that the request is served with
	 */
	@Override
	public String getLanguageId(HttpServletRequest request) {
		String languageId = ParamUtil.getString(request, "languageId");

		if (Validator.isNotNull(languageId)) {
			CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

			if (companyLocalesBag.containsLanguageCode(languageId) ||
				companyLocalesBag.containsLanguageId(languageId)) {

				return languageId;
			}
		}

		Locale locale = PortalUtil.getLocale(request, null, false);

		return getLanguageId(locale);
	}

	/**
	 * Returns the language ID from the locale. The language ID is returned as a
	 * language code (e.g. <code>en</code>) or a specific variant (e.g.
	 * <code>en_GB</code>).
	 *
	 * @param  locale the locale used to determine the language ID
	 * @return the language ID from the locale
	 */
	@Override
	public String getLanguageId(Locale locale) {
		return LocaleUtil.toLanguageId(locale);
	}

	/**
	 * Returns the language ID that the {@link PortletRequest} is served with.
	 * The language ID is returned as a language code (e.g. <code>en</code>) or
	 * a specific variant (e.g. <code>en_GB</code>).
	 *
	 * @param  portletRequest the portlet request used to determine the language
	 *         ID
	 * @return the language ID that the portlet request is served with
	 */
	@Override
	public String getLanguageId(PortletRequest portletRequest) {
		HttpServletRequest request = PortalUtil.getHttpServletRequest(
			portletRequest);

		return getLanguageId(request);
	}

	/**
	 * Returns the last time in milliseconds when there was any change in the
	 * languages list company or group
	 *
	 * @return the last moodified time in milliseconds
	 * @review
	 */
	@Override
	public long getLastModified() {
		return _lastModified;
	}

	@Override
	public Locale getLocale(long groupId, String languageCode) {
		try {
			if (isInheritLocales(groupId)) {
				return getLocale(languageCode);
			}
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn("Unable to check if group inherits locales");
			}
		}

		Map<String, Locale> groupLanguageCodeLocalesMap =
			_getGroupLanguageCodeLocalesMap(groupId);

		return groupLanguageCodeLocalesMap.get(languageCode);
	}

	/**
	 * Returns the locale associated with the language code.
	 *
	 * @param  languageCode the code representation of a language (e.g.
	 *         <code>en</code> and <code>en_GB</code>)
	 * @return the locale associated with the language code
	 */
	@Override
	public Locale getLocale(String languageCode) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.getByLanguageCode(languageCode);
	}

	@Override
	public ResourceBundleLoader getPortalResourceBundleLoader() {
		return LanguageResources.RESOURCE_BUNDLE_LOADER;
	}

	@Override
	public Set<Locale> getSupportedLocales() {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag._supportedLocalesSet;
	}

	/**
	 * Returns an exact localized description of the time interval (in
	 * milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * For example, the following time intervals would be converted to the
	 * following time descriptions, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @return an exact localized description of the time interval in the
	 *         largest unit possible
	 */
	@Override
	public String getTimeDescription(
		HttpServletRequest request, long milliseconds) {

		return getTimeDescription(request, milliseconds, false);
	}

	/**
	 * Returns an approximate or exact localized description of the time
	 * interval (in milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * Approximate descriptions round the time to the largest possible unit and
	 * ignores the rest. For example, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * Any time interval 1000-1999 = 1 Second
	 * </li>
	 * <li>
	 * Any time interval 86400000-172799999 = 1 Day
	 * </li>
	 * </ul>
	 *
	 * <p>
	 * Otherwise, exact descriptions would follow a similar conversion pattern
	 * as below:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @param  approximate whether the time description is approximate
	 * @return a localized description of the time interval in the largest unit
	 *         possible
	 */
	@Override
	public String getTimeDescription(
		HttpServletRequest request, long milliseconds, boolean approximate) {

		String description = Time.getDescription(milliseconds, approximate);

		String value = null;

		try {
			String[] parts = description.split(StringPool.SPACE, 2);

			String unit = StringUtil.toLowerCase(parts[1]);

			if (unit.equals("second")) {
				unit += "[time]";
			}

			value = format(request, "x-" + unit, parts[0]);
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns an exact localized description of the time interval (in
	 * milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * For example, the following time intervals would be converted to the
	 * following time descriptions, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  request the request used to determine the current locale
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @return an exact localized description of the time interval in the
	 *         largest unit possible
	 */
	@Override
	public String getTimeDescription(
		HttpServletRequest request, Long milliseconds) {

		return getTimeDescription(request, milliseconds.longValue());
	}

	/**
	 * Returns an exact localized description of the time interval (in
	 * milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * For example, the following time intervals would be converted to the
	 * following time descriptions, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  locale the locale used to determine the language
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @return an exact localized description of the time interval in the
	 *         largest unit possible
	 */
	@Override
	public String getTimeDescription(Locale locale, long milliseconds) {
		return getTimeDescription(locale, milliseconds, false);
	}

	/**
	 * Returns an approximate or exact localized description of the time
	 * interval (in milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * Approximate descriptions round the time to the largest possible unit and
	 * ignores the rest. For example, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * Any time interval 1000-1999 = 1 Second
	 * </li>
	 * <li>
	 * Any time interval 86400000-172799999 = 1 Day
	 * </li>
	 * </ul>
	 *
	 * <p>
	 * Otherwise, exact descriptions would follow a similar conversion pattern
	 * as below:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  locale the locale used to determine the language
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @param  approximate whether the time description is approximate
	 * @return a localized description of the time interval in the largest unit
	 *         possible
	 */
	@Override
	public String getTimeDescription(
		Locale locale, long milliseconds, boolean approximate) {

		String description = Time.getDescription(milliseconds, approximate);

		String value = null;

		try {
			String[] parts = description.split(StringPool.SPACE, 2);

			String unit = StringUtil.toLowerCase(parts[1]);

			if (unit.equals("second")) {
				unit += "[time]";
			}

			value = format(locale, "x-" + unit, parts[0]);
		}
		catch (Exception e) {
			if (_log.isWarnEnabled()) {
				_log.warn(e, e);
			}
		}

		return value;
	}

	/**
	 * Returns an exact localized description of the time interval (in
	 * milliseconds) in the largest unit possible.
	 *
	 * <p>
	 * For example, the following time intervals would be converted to the
	 * following time descriptions, using the English locale:
	 * </p>
	 *
	 * <ul>
	 * <li>
	 * 1000 = 1 Second
	 * </li>
	 * <li>
	 * 1001 = 1001 Milliseconds
	 * </li>
	 * <li>
	 * 86400000 = 1 Day
	 * </li>
	 * <li>
	 * 86401000 = 86401 Seconds
	 * </li>
	 * </ul>
	 *
	 * @param  locale the locale used to determine the language
	 * @param  milliseconds the time interval in milliseconds to describe
	 * @return an exact localized description of the time interval in the
	 *         largest unit possible
	 */
	@Override
	public String getTimeDescription(Locale locale, Long milliseconds) {
		return getTimeDescription(locale, milliseconds.longValue());
	}

	@Override
	public void init() {
		_companyLocalesBags.clear();
	}

	/**
	 * Returns <code>true</code> if the language code is configured to be
	 * available. Locales can be configured in <code>portal.properties</code>
	 * using the <code>locales</code> and <code>locales.enabled</code> keys.
	 *
	 * @param  languageCode the code representation of a language (e.g.
	 *         <code>en</code> and <code>en_GB</code>) to search for
	 * @return <code>true</code> if the language code is configured to be
	 *         available; <code>false</code> otherwise
	 */
	@Override
	public boolean isAvailableLanguageCode(String languageCode) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.containsLanguageCode(languageCode);
	}

	/**
	 * Returns <code>true</code> if the locale is configured to be available.
	 * Locales can be configured in <code>portal.properties</code> using the
	 * <code>locales</code> and <code>locales.enabled</code> keys.
	 *
	 * @param  locale the locale to search for
	 * @return <code>true</code> if the locale is configured to be available;
	 *         <code>false</code> otherwise
	 */
	@Override
	public boolean isAvailableLocale(Locale locale) {
		if (locale == null) {
			return false;
		}

		return isAvailableLocale(LocaleUtil.toLanguageId(locale));
	}

	/**
	 * Returns <code>true</code> if the locale is configured to be available in
	 * the group.
	 *
	 * @param  groupId the primary key of the group
	 * @param  locale the locale to search for
	 * @return <code>true</code> if the locale is configured to be available in
	 *         the group; <code>false</code> otherwise
	 */
	@Override
	public boolean isAvailableLocale(long groupId, Locale locale) {
		if (locale == null) {
			return false;
		}

		return isAvailableLocale(groupId, LocaleUtil.toLanguageId(locale));
	}

	/**
	 * Returns <code>true</code> if the language ID is configured to be
	 * available in the group.
	 *
	 * @param  groupId the primary key of the group
	 * @param  languageId the language ID to search for
	 * @return <code>true</code> if the language ID is configured to be
	 *         available in the group; <code>false</code> otherwise
	 */
	@Override
	public boolean isAvailableLocale(long groupId, String languageId) {
		if (groupId <= 0) {
			return isAvailableLocale(languageId);
		}

		try {
			if (isInheritLocales(groupId)) {
				Group group = GroupLocalServiceUtil.getGroup(groupId);

				CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag(
					group.getCompanyId());

				return companyLocalesBag.containsLanguageId(languageId);
			}
		}
		catch (Exception e) {
		}

		Map<String, Locale> groupLanguageIdLocalesMap =
			_getGroupLanguageIdLocalesMap(groupId);

		return groupLanguageIdLocalesMap.containsKey(languageId);
	}

	/**
	 * Returns <code>true</code> if the language ID is configured to be
	 * available.
	 *
	 * @param  languageId the language ID to search for
	 * @return <code>true</code> if the language ID is configured to be
	 *         available; <code>false</code> otherwise
	 */
	@Override
	public boolean isAvailableLocale(String languageId) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.containsLanguageId(languageId);
	}

	/**
	 * Returns <code>true</code> if the locale is configured to be a beta
	 * language.
	 *
	 * @param  locale the locale to search for
	 * @return <code>true</code> if the locale is configured to be a beta
	 *         language; <code>false</code> otherwise
	 */
	@Override
	public boolean isBetaLocale(Locale locale) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.isBetaLocale(locale);
	}

	@Override
	public boolean isDuplicateLanguageCode(String languageCode) {
		CompanyLocalesBag companyLocalesBag = _getCompanyLocalesBag();

		return companyLocalesBag.isDuplicateLanguageCode(languageCode);
	}

	@Override
	public boolean isInheritLocales(long groupId) throws PortalException {
		Group group = GroupLocalServiceUtil.getGroup(groupId);

		if (group.isStagingGroup()) {
			group = group.getLiveGroup();
		}

		if (!group.isSite() || group.isCompany()) {
			return true;
		}

		return GetterUtil.getBoolean(
			group.getTypeSettingsProperty(
				GroupConstants.TYPE_SETTINGS_KEY_INHERIT_LOCALES),
			true);
	}

	@Override
	public boolean isSameLanguage(Locale locale1, Locale locale2) {
		if ((locale1 == null) || (locale2 == null)) {
			return false;
		}

		String language1 = locale1.getLanguage();
		String language2 = locale2.getLanguage();

		return language1.equals(language2);
	}

	@Override
	public String process(
		ResourceBundle resourceBundle, Locale locale, String content) {

		StringBundler sb = new StringBundler();

		Matcher matcher = _pattern.matcher(content);

		int x = 0;

		while (matcher.find()) {
			int y = matcher.start(0);

			String key = matcher.group(1);

			sb.append(content.substring(x, y));
			sb.append(StringPool.APOSTROPHE);

			String value = get(resourceBundle, key);

			sb.append(HtmlUtil.escapeJS(value));

			sb.append(StringPool.APOSTROPHE);

			x = matcher.end(0);
		}

		sb.append(content.substring(x));

		return sb.toString();
	}

	@Override
	public void resetAvailableGroupLocales(long groupId) {
		_resetAvailableGroupLocales(groupId);
	}

	@Override
	public void resetAvailableLocales(long companyId) {
		_resetAvailableLocales(companyId);
	}

	@Override
	public void updateCookie(
		HttpServletRequest request, HttpServletResponse response,
		Locale locale) {

		String languageId = LocaleUtil.toLanguageId(locale);

		Cookie languageIdCookie = new Cookie(
			CookieKeys.GUEST_LANGUAGE_ID, languageId);

		String domain = CookieKeys.getDomain(request);

		if (Validator.isNotNull(domain)) {
			languageIdCookie.setDomain(domain);
		}

		languageIdCookie.setMaxAge(CookieKeys.MAX_AGE);
		languageIdCookie.setPath(StringPool.SLASH);

		CookieKeys.addCookie(request, response, languageIdCookie);
	}

	private static CompanyLocalesBag _getCompanyLocalesBag() {
		Long companyId = CompanyThreadLocal.getCompanyId();

		return _getCompanyLocalesBag(companyId);
	}

	private static CompanyLocalesBag _getCompanyLocalesBag(long companyId) {
		CompanyLocalesBag companyLocalesBag = _companyLocalesBags.get(
			companyId);

		if (companyLocalesBag == null) {
			companyLocalesBag = new CompanyLocalesBag(companyId);

			_companyLocalesBags.put(companyId, companyLocalesBag);
		}

		return companyLocalesBag;
	}

	private static void _updateLastModified() {
		_lastModified = System.currentTimeMillis();
	}

	private ObjectValuePair<HashMap<String, Locale>, HashMap<String, Locale>>
		_createGroupLocales(long groupId) {

		String[] languageIds = PropsValues.LOCALES_ENABLED;

		Locale defaultLocale = LocaleUtil.getDefault();

		try {
			Group group = GroupLocalServiceUtil.getGroup(groupId);

			defaultLocale = PortalUtil.getSiteDefaultLocale(group);

			UnicodeProperties typeSettingsProperties =
				group.getTypeSettingsProperties();

			String groupLanguageIds = typeSettingsProperties.getProperty(
				PropsKeys.LOCALES);

			if (groupLanguageIds != null) {
				languageIds = StringUtil.split(groupLanguageIds);
			}
		}
		catch (Exception e) {
		}

		HashMap<String, Locale> groupLanguageCodeLocalesMap = new HashMap<>();
		HashMap<String, Locale> groupLanguageIdLocalesMap =
			new LinkedHashMap<>();

		groupLanguageCodeLocalesMap.put(
			defaultLocale.getLanguage(), defaultLocale);

		for (String languageId : languageIds) {
			Locale locale = LocaleUtil.fromLanguageId(languageId, false);

			String languageCode = languageId;

			int pos = languageId.indexOf(CharPool.UNDERLINE);

			if (pos > 0) {
				languageCode = languageId.substring(0, pos);
			}

			if (!groupLanguageCodeLocalesMap.containsKey(languageCode)) {
				groupLanguageCodeLocalesMap.put(languageCode, locale);
			}

			groupLanguageIdLocalesMap.put(languageId, locale);
		}

		_groupLanguageCodeLocalesMapMap.put(
			groupId, groupLanguageCodeLocalesMap);
		_groupLanguageIdLocalesMap.put(groupId, groupLanguageIdLocalesMap);

		_updateLastModified();

		return new ObjectValuePair<>(
			groupLanguageCodeLocalesMap, groupLanguageIdLocalesMap);
	}

	private String _decorateMessageFormat(
		HttpServletRequest request, String pattern,
		Object[] formattedArguments) {

		Locale locale = _getLocale(request);

		return _decorateMessageFormat(locale, pattern, formattedArguments);
	}

	private String _decorateMessageFormat(
		Locale locale, String pattern, Object[] formattedArguments) {

		if (locale == null) {
			locale = LocaleUtil.getDefault();
		}

		String value = _getFastFormattedMessage(
			locale, pattern, formattedArguments);

		if (value != null) {
			return value;
		}

		pattern = _escapePattern(pattern);

		MessageFormat messageFormat = new MessageFormat(pattern, locale);

		for (int i = 0; i < formattedArguments.length; i++) {
			Object formattedArgument = formattedArguments[i];

			if (formattedArgument instanceof Number) {
				messageFormat.setFormat(i, NumberFormat.getInstance(locale));
			}
		}

		return messageFormat.format(formattedArguments);
	}

	private String _escapePattern(String pattern) {
		return StringUtil.replace(
			pattern, CharPool.APOSTROPHE, StringPool.DOUBLE_APOSTROPHE);
	}

	private String _get(ResourceBundle resourceBundle, String key) {
		if (PropsValues.TRANSLATIONS_DISABLED) {
			return key;
		}

		if ((resourceBundle == null) || (key == null)) {
			return null;
		}

		String value = ResourceBundleUtil.getString(resourceBundle, key);

		if (value != null) {
			return LanguageResources.fixValue(value);
		}

		if ((key.length() > 0) &&
			(key.charAt(key.length() - 1) == CharPool.CLOSE_BRACKET)) {

			int pos = key.lastIndexOf(CharPool.OPEN_BRACKET);

			if (pos != -1) {
				key = key.substring(0, pos);

				return _get(resourceBundle, key);
			}
		}

		return null;
	}

	private String _getFastFormattedMessage(
		Locale locale, String pattern, Object[] arguments) {

		Format dateFormat = null;
		Format numberFormat = null;
		int pos = 0;
		StringBuilder sb = new StringBuilder(
			(16 * arguments.length) + pattern.length());

		int start = pattern.indexOf(CharPool.OPEN_CURLY_BRACE);

		while (start != -1) {
			int endIndex = start + 2;

			if ((endIndex > pattern.length()) ||
				(pattern.charAt(endIndex) != CharPool.CLOSE_CURLY_BRACE)) {

				return null;
			}

			int argumentIndex = pattern.charAt(start + 1) - CharPool.NUMBER_0;

			if ((argumentIndex < 0) || (arguments.length <= argumentIndex)) {
				return null;
			}

			sb.append(pattern, pos, start);

			Object argument = arguments[argumentIndex];

			if (argument instanceof Number) {
				if (numberFormat == null) {
					numberFormat = NumberFormat.getNumberInstance(locale);
				}

				sb.append(numberFormat.format(argument));
			}
			else if (argument instanceof Date) {
				if (dateFormat == null) {
					dateFormat = FastDateFormatFactoryUtil.getDateTime(
						FastDateFormatConstants.SHORT,
						FastDateFormatConstants.LONG, locale, null);
				}

				sb.append(dateFormat.format(argument));
			}
			else {
				sb.append(argument);
			}

			pos = endIndex + 1;

			start = pattern.indexOf(CharPool.OPEN_CURLY_BRACE, pos);
		}

		if (pos < pattern.length()) {
			sb.append(pattern, pos, pattern.length());
		}

		return sb.toString();
	}

	private Map<String, Locale> _getGroupLanguageCodeLocalesMap(long groupId) {
		Map<String, Locale> groupLanguageCodeLocalesMap =
			_groupLanguageCodeLocalesMapMap.get(groupId);

		if (groupLanguageCodeLocalesMap == null) {
			ObjectValuePair<HashMap<String, Locale>, HashMap<String, Locale>>
				objectValuePair = _createGroupLocales(groupId);

			groupLanguageCodeLocalesMap = objectValuePair.getKey();
		}

		return groupLanguageCodeLocalesMap;
	}

	private Map<String, Locale> _getGroupLanguageIdLocalesMap(long groupId) {
		Map<String, Locale> groupLanguageIdLocalesMap =
			_groupLanguageIdLocalesMap.get(groupId);

		if (groupLanguageIdLocalesMap == null) {
			ObjectValuePair<HashMap<String, Locale>, HashMap<String, Locale>>
				objectValuePair = _createGroupLocales(groupId);

			groupLanguageIdLocalesMap = objectValuePair.getValue();
		}

		return groupLanguageIdLocalesMap;
	}

	private Locale _getLocale(HttpServletRequest request) {
		Locale locale = null;

		ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
			WebKeys.THEME_DISPLAY);

		if (themeDisplay != null) {
			locale = themeDisplay.getLocale();
		}
		else {
			locale = request.getLocale();

			if (!isAvailableLocale(locale)) {
				locale = LocaleUtil.getDefault();
			}
		}

		return locale;
	}

	private void _resetAvailableGroupLocales(long groupId) {
		_groupLocalesPortalCache.remove(groupId);

		_updateLastModified();
	}

	private void _resetAvailableLocales(long companyId) {
		_companyLocalesPortalCache.remove(companyId);

		_updateLastModified();
	}

	private static final String _COMPANY_LOCALES_PORTAL_CACHE_NAME =
		LanguageImpl.class.getName() + "._companyLocalesPortalCache";

	private static final String _GROUP_LOCALES_PORTAL_CACHE_NAME =
		LanguageImpl.class.getName() + "._groupLocalesPortalCache";

	private static final Log _log = LogFactoryUtil.getLog(LanguageImpl.class);

	private static final Map<Long, CompanyLocalesBag> _companyLocalesBags =
		new ConcurrentHashMap<>();
	private static PortalCache<Long, Serializable> _companyLocalesPortalCache;
	private static PortalCache<Long, Serializable> _groupLocalesPortalCache;
	private static volatile long _lastModified = System.currentTimeMillis();
	private static final Pattern _pattern = Pattern.compile(
		"Liferay\\s*\\.\\s*Language\\s*\\.\\s*get\\s*" +
			"\\(\\s*[\"']([^)]+)[\"']\\s*\\)",
		Pattern.MULTILINE);

	private static final Synchronizer<Long, Serializable> _removeSynchronizer =
		new Synchronizer<Long, Serializable>() {

			@Override
			public void onSynchronize(
				Map<? extends Long, ? extends Serializable> map, Long key,
				Serializable value, int timeToLive) {

				map.remove(key);
			}

		};

	private final Map<Long, HashMap<String, Locale>>
		_groupLanguageCodeLocalesMapMap = new ConcurrentHashMap<>();
	private final Map<Long, HashMap<String, Locale>>
		_groupLanguageIdLocalesMap = new ConcurrentHashMap<>();

	private static class CompanyLocalesBag implements Serializable {

		public boolean containsLanguageCode(String languageCode) {
			return _languageCodeLocalesMap.containsKey(languageCode);
		}

		public boolean containsLanguageId(String languageId) {
			return _languageIdLocalesMap.containsKey(languageId);
		}

		public Set<Locale> getAvailableLocales() {
			return _availableLocales;
		}

		public Locale getByLanguageCode(String languageCode) {
			return _languageCodeLocalesMap.get(languageCode);
		}

		public boolean isBetaLocale(Locale locale) {
			return _localesBetaSet.contains(locale);
		}

		public boolean isDuplicateLanguageCode(String languageCode) {
			return _duplicateLanguageCodes.contains(languageCode);
		}

		private CompanyLocalesBag(long companyId) {
			String[] languageIds = PropsValues.LOCALES;

			if (companyId != CompanyConstants.SYSTEM) {
				try {
					languageIds = PrefsPropsUtil.getStringArray(
						companyId, PropsKeys.LOCALES, StringPool.COMMA,
						PropsValues.LOCALES_ENABLED);
				}
				catch (SystemException se) {

					// LPS-52675

					if (_log.isDebugEnabled()) {
						_log.debug(se, se);
					}

					languageIds = PropsValues.LOCALES_ENABLED;
				}
			}

			Locale defaultLocale = LocaleUtil.getDefault();

			String defaultLanguageId = LocaleUtil.toLanguageId(defaultLocale);

			_languageCodeLocalesMap.put(
				defaultLocale.getLanguage(), defaultLocale);

			_languageIdLocalesMap.put(defaultLanguageId, defaultLocale);

			languageIds = ArrayUtil.remove(languageIds, defaultLanguageId);

			Set<String> duplicateLanguageCodes = new HashSet<>();

			for (String languageId : languageIds) {
				Locale locale = LocaleUtil.fromLanguageId(languageId, false);

				String languageCode = languageId;

				int pos = languageId.indexOf(CharPool.UNDERLINE);

				if (pos > 0) {
					languageCode = languageId.substring(0, pos);
				}

				if (_languageCodeLocalesMap.containsKey(languageCode)) {
					duplicateLanguageCodes.add(languageCode);
				}
				else {
					_languageCodeLocalesMap.put(languageCode, locale);
				}

				_languageIdLocalesMap.put(languageId, locale);
			}

			if (duplicateLanguageCodes.isEmpty()) {
				_duplicateLanguageCodes = Collections.emptySet();
			}
			else {
				_duplicateLanguageCodes = duplicateLanguageCodes;
			}

			for (String languageId : PropsValues.LOCALES_BETA) {
				_localesBetaSet.add(
					LocaleUtil.fromLanguageId(languageId, false));
			}

			_availableLocales = Collections.unmodifiableSet(
				new LinkedHashSet<>(_languageIdLocalesMap.values()));

			Set<Locale> supportedLocalesSet = new HashSet<>(
				_languageIdLocalesMap.values());

			supportedLocalesSet.removeAll(_localesBetaSet);

			_supportedLocalesSet = Collections.unmodifiableSet(
				supportedLocalesSet);
		}

		private final Set<Locale> _availableLocales;
		private final Set<String> _duplicateLanguageCodes;
		private final Map<String, Locale> _languageCodeLocalesMap =
			new HashMap<>();
		private final Map<String, Locale> _languageIdLocalesMap =
			new LinkedHashMap<>();
		private final Set<Locale> _localesBetaSet = new HashSet<>();
		private final Set<Locale> _supportedLocalesSet;

	}

}