I18nService.java
- /*
- * Copyright (c) 2002-2022, City of Paris
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice
- * and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice
- * and the following disclaimer in the documentation and/or other materials
- * provided with the distribution.
- *
- * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * License 1.0
- */
- package fr.paris.lutece.portal.service.i18n;
- import fr.paris.lutece.portal.service.util.AppException;
- import fr.paris.lutece.portal.service.util.AppLogService;
- import fr.paris.lutece.portal.service.util.AppPathService;
- import fr.paris.lutece.portal.service.util.AppPropertiesService;
- import fr.paris.lutece.util.ReferenceList;
- import java.io.File;
- import java.net.MalformedURLException;
- import java.net.URL;
- import java.net.URLClassLoader;
- import java.text.DateFormat;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.MissingResourceException;
- import java.util.ResourceBundle;
- import java.util.StringTokenizer;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- /**
- * This class provides services for internationalization (i18n)
- *
- * @since v1.4.1
- */
- public final class I18nService
- {
- private static final String FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION = "fr.paris.lutece.portal.resources.{0}_messages";
- private static final String FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.resources.{0}_messages";
- private static final String FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.modules.{1}.resources.{1}_messages";
- private static final Pattern PATTERN_LOCALIZED_KEY = Pattern.compile( "#i18n\\{(.*?)\\}" );
- private static final String PROPERTY_AVAILABLES_LOCALES = "lutece.i18n.availableLocales";
- private static final Locale LOCALE_DEFAULT = new Locale( "", "", "" );
- private static final String PROPERTY_DEFAULT_LOCALE = "lutece.i18n.defaultLocale";
- private static final String PROPERTY_FORMAT_DATE_SHORT_LIST = "lutece.format.date.short";
- private static Map<String, String> _pluginBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
- private static Map<String, String> _moduleBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
- private static Map<String, String> _portalBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
- private static final String PROPERTY_PATH_OVERRIDE = "path.i18n.override";
- private static final ClassLoader _overrideLoader;
- private static final Map<String, ResourceBundle> _resourceBundleCache = Collections.synchronizedMap( new HashMap<String, ResourceBundle>( ) );
- static
- {
- File overridePath = null;
- try
- {
- overridePath = new File( AppPathService.getPath( PROPERTY_PATH_OVERRIDE ) );
- }
- catch( AppException e )
- {
- // the key is unknown. Message override will be deactivated
- AppLogService.error( "property {} is undefined. Message overriding will be disabled.", PROPERTY_PATH_OVERRIDE );
- }
- URL [ ] overrideURL = null;
- if ( overridePath != null )
- {
- try
- {
- overrideURL = new URL [ ] {
- overridePath.toURI( ).toURL( )
- };
- }
- catch( MalformedURLException e )
- {
- AppLogService.error( "Error initializing message overriding: {}", e.getMessage( ), e );
- }
- }
- if ( overrideURL != null )
- {
- _overrideLoader = new URLClassLoader( overrideURL, null );
- }
- else
- {
- _overrideLoader = null;
- }
- }
- /**
- * Private constructor
- */
- private I18nService( )
- {
- }
- /**
- * This method localize a string. It scans for localization keys and replace them by localized values.<br>
- * The localization key structure is : #{bundle.key}.<br>
- * bundle's values should be 'portal' or a plugin name.
- *
- * @param strSource
- * The string that contains localization keys
- * @param locale
- * The locale
- * @return The localized string
- */
- public static String localize( String strSource, Locale locale )
- {
- String result = strSource;
- if ( strSource != null )
- {
- Matcher matcher = PATTERN_LOCALIZED_KEY.matcher( strSource );
- if ( matcher.find( ) )
- {
- StringBuffer sb = new StringBuffer( );
- do
- {
- matcher.appendReplacement( sb, getLocalizedString( matcher.group( 1 ), locale ) );
- }
- while ( matcher.find( ) );
- matcher.appendTail( sb );
- result = sb.toString( );
- }
- }
- return result;
- }
- /**
- * Returns the string corresponding to a given key for a given locale <br>
- * <ul>
- * <li>Core key structure :<br>
- * <code>portal.[admin, features, insert, rbac, search, site, style, system, users, util].key</code></li>
- * <li>Plugin key structure :<br>
- * <code>[plugin].key </code></li>
- * <li>Module key structure :<br>
- * <code>module.[plugin].[module].key </code></li>
- * </ul>
- *
- * @param strKey
- * The key of the string
- * @param theLocale
- * The locale
- * @return The string corresponding to the key
- */
- public static String getLocalizedString( String strKey, Locale theLocale )
- {
- Locale locale = theLocale;
- String strReturn = "";
- try
- {
- int nPos = strKey.indexOf( '.' );
- if ( nPos != -1 )
- {
- String strBundleKey = strKey.substring( 0, nPos );
- String strStringKey = strKey.substring( nPos + 1 );
- String strBundle;
- if ( !strBundleKey.equals( "portal" ) )
- {
- if ( strBundleKey.equals( "module" ) )
- {
- // module resource
- nPos = strStringKey.indexOf( '.' );
- String strPlugin = strStringKey.substring( 0, nPos );
- strStringKey = strStringKey.substring( nPos + 1 );
- nPos = strStringKey.indexOf( '.' );
- String strModule = strStringKey.substring( 0, nPos );
- strStringKey = strStringKey.substring( nPos + 1 );
- strBundle = getModuleBundleName( strPlugin, strModule );
- }
- else
- {
- // plugin resource
- strBundle = getPluginBundleName( strBundleKey );
- }
- }
- else
- {
- nPos = strStringKey.indexOf( '.' );
- String strElement = strStringKey.substring( 0, nPos );
- strStringKey = strStringKey.substring( nPos + 1 );
- strBundle = getPortalBundleName( strElement );
- }
- // if language is english use a special locale to force using default
- // bundle instead of the bundle of default locale.
- if ( locale.getLanguage( ).equals( Locale.ENGLISH.getLanguage( ) ) )
- {
- locale = LOCALE_DEFAULT;
- }
- ResourceBundle rbLabels = getResourceBundle( locale, strBundle );
- strReturn = rbLabels.getString( strStringKey );
- }
- }
- catch( Exception e )
- {
- String strErrorMessage = "Error localizing key : '" + strKey + "' - " + e.getMessage( );
- if ( e.getCause( ) != null )
- {
- strErrorMessage += ( " - cause : " + e.getCause( ).getMessage( ) );
- }
- AppLogService.error( strErrorMessage );
- }
- return strReturn;
- }
- /**
- * Get resource bundle name for plugin
- *
- * @param strBundleKey
- * the plugin key
- * @return resource bundle name
- */
- private static String getPluginBundleName( String strBundleKey )
- {
- return _pluginBundleNames.computeIfAbsent( strBundleKey, s -> new MessageFormat( FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION ).format( new String [ ] {
- s
- } ) );
- }
- /**
- * Get resource bundle name for module
- *
- * @param strPlugin
- * the plugin key
- * @param strModule
- * the module key
- * @return resource bundle name
- */
- private static String getModuleBundleName( String strPlugin, String strModule )
- {
- String key = strPlugin + strModule;
- return _moduleBundleNames.computeIfAbsent( key, s -> new MessageFormat( FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION ).format( new String [ ] {
- strPlugin, strModule
- } ) );
- }
- /**
- * Get resource bundle name for core element
- *
- * @param strElement
- * element name
- * @return resource bundle name
- */
- private static String getPortalBundleName( String strElement )
- {
- return _portalBundleNames.computeIfAbsent( strElement, s -> new MessageFormat( FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION ).format( new String [ ] {
- s
- } ) );
- }
- /**
- * Returns the string corresponding to a given key for a given locale that use a MessageFormat pattern with arguments.
- *
- * @return The string corresponding to the key
- * @param arguments
- * The arguments used as values by the formatter
- * @param strKey
- * The key of the string that contains the pattern
- * @param locale
- * The locale
- */
- public static String getLocalizedString( String strKey, Object [ ] arguments, Locale locale )
- {
- String strMessagePattern = getLocalizedString( strKey, locale );
- return MessageFormat.format( strMessagePattern, arguments );
- }
- /**
- * Format a date according to the given locale
- *
- * @param date
- * The date to format
- * @param locale
- * The locale
- * @param nDateFormat
- * A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
- * @return The formatted date
- */
- public static String getLocalizedDate( Date date, Locale locale, int nDateFormat )
- {
- DateFormat dateFormatter = DateFormat.getDateInstance( nDateFormat, locale );
- return dateFormatter.format( date );
- }
- /**
- * Format a date according to the given locale
- *
- * @param date
- * The date to format
- * @param locale
- * The locale
- * @param nDateFormat
- * A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
- * @param nTimeFormat
- * A TimeFormat constant corresponding to the expected format. (ie: DateFormat.SHORT)
- * @return The formatted date
- */
- public static String getLocalizedDateTime( Date date, Locale locale, int nDateFormat, int nTimeFormat )
- {
- DateFormat dateFormatter = DateFormat.getDateTimeInstance( nDateFormat, nTimeFormat, locale );
- return dateFormatter.format( date );
- }
- /**
- * Returns supported locales for Lutece backoffice
- *
- * @return A list of locales
- */
- public static List<Locale> getAdminAvailableLocales( )
- {
- String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_AVAILABLES_LOCALES );
- StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
- List<Locale> list = new ArrayList<>( );
- while ( strTokens.hasMoreTokens( ) )
- {
- String strLanguage = strTokens.nextToken( );
- Locale locale = new Locale( strLanguage );
- list.add( locale );
- }
- return list;
- }
- /**
- * Get the default Locale specified in properties file
- *
- * @return The default Locale
- */
- public static Locale getDefaultLocale( )
- {
- String strDefaultLocale = AppPropertiesService.getProperty( PROPERTY_DEFAULT_LOCALE );
- return new Locale( strDefaultLocale );
- }
- /**
- * Get the short date format specified by a locale
- *
- * @param locale
- * The locale
- * @return The localized short date pattern or null else
- */
- public static String getDateFormatShortPattern( Locale locale )
- {
- String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_FORMAT_DATE_SHORT_LIST );
- if ( ( locale != null ) && ( strAvailableLocales != null ) && !strAvailableLocales.equals( "" ) )
- {
- StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
- String strToken = null;
- for ( Locale adminLocale : getAdminAvailableLocales( ) )
- {
- if ( strTokens.hasMoreTokens( ) )
- {
- strToken = strTokens.nextToken( );
- }
- if ( adminLocale.getLanguage( ).equals( locale.getLanguage( ) ) )
- {
- return strToken;
- }
- }
- }
- return null;
- }
- /**
- * Returns a ReferenceList of available locales
- *
- * @param locale
- * The locale to display available languages
- * @return A ReferenceList of available locales
- */
- public static ReferenceList getAdminLocales( Locale locale )
- {
- ReferenceList list = new ReferenceList( );
- for ( Locale l : getAdminAvailableLocales( ) )
- {
- list.addItem( l.getLanguage( ), l.getDisplayLanguage( l ) );
- }
- return list;
- }
- /**
- * Localize in place all items of a collection
- *
- * @param <E>
- * the type of the elements of the collection
- *
- * @param collection
- * The collection to localize
- * @param locale
- * The locale
- * @return The localized collection passed as argument
- */
- public static <E extends Localizable> Collection<E> localizeCollection( Collection<E> collection, Locale locale )
- {
- for ( Localizable object : collection )
- {
- object.setLocale( locale );
- }
- return collection;
- }
- /**
- * Localize in place all items of a list
- *
- * @param <E>
- * the type of the elements of the list
- *
- * @param list
- * The list to localize
- * @param locale
- * The locale
- * @return The localized collection passed as argument
- */
- public static <E extends Localizable> List<E> localizeCollection( List<E> list, Locale locale )
- {
- for ( Localizable object : list )
- {
- object.setLocale( locale );
- }
- return list;
- }
- /**
- * get the resource bundle, possibly with its override
- *
- * @param locale
- * the locale
- * @param strBundle
- * the bundle name
- * @return the resource bundle
- */
- private static ResourceBundle getResourceBundle( Locale locale, String strBundle )
- {
- String key = strBundle + locale.toString( );
- return _resourceBundleCache.computeIfAbsent( key, k -> createResourceBundle( strBundle, locale ) );
- }
- private static ResourceBundle createResourceBundle( String strBundle, Locale locale )
- {
- ResourceBundle rbLabels = ResourceBundle.getBundle( strBundle, locale );
- if ( _overrideLoader != null )
- {
- ResourceBundle overrideBundle = null;
- try
- {
- overrideBundle = ResourceBundle.getBundle( strBundle, locale, _overrideLoader );
- }
- catch( MissingResourceException e )
- {
- // no override for this resource
- return rbLabels;
- }
- return new CombinedResourceBundle( overrideBundle, rbLabels );
- }
- return rbLabels;
- }
- /**
- * Reset the caches
- *
- * @since 5.1
- */
- public static void resetCache( )
- {
- ResourceBundle.clearCache( );
- if ( _overrideLoader != null )
- {
- ResourceBundle.clearCache( _overrideLoader );
- }
- _resourceBundleCache.clear( );
- }
- }