I18nService.java

  1. /*
  2.  * Copyright (c) 2002-2022, City of Paris
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions
  7.  * are met:
  8.  *
  9.  *  1. Redistributions of source code must retain the above copyright notice
  10.  *     and the following disclaimer.
  11.  *
  12.  *  2. Redistributions in binary form must reproduce the above copyright notice
  13.  *     and the following disclaimer in the documentation and/or other materials
  14.  *     provided with the distribution.
  15.  *
  16.  *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
  17.  *     contributors may be used to endorse or promote products derived from
  18.  *     this software without specific prior written permission.
  19.  *
  20.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  21.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  22.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  23.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
  24.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  25.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  26.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  27.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  28.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  29.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  30.  * POSSIBILITY OF SUCH DAMAGE.
  31.  *
  32.  * License 1.0
  33.  */
  34. package fr.paris.lutece.portal.service.i18n;

  35. import fr.paris.lutece.portal.service.util.AppException;
  36. import fr.paris.lutece.portal.service.util.AppLogService;
  37. import fr.paris.lutece.portal.service.util.AppPathService;
  38. import fr.paris.lutece.portal.service.util.AppPropertiesService;
  39. import fr.paris.lutece.util.ReferenceList;

  40. import java.io.File;

  41. import java.net.MalformedURLException;
  42. import java.net.URL;
  43. import java.net.URLClassLoader;

  44. import java.text.DateFormat;
  45. import java.text.MessageFormat;

  46. import java.util.ArrayList;
  47. import java.util.Collection;
  48. import java.util.Collections;
  49. import java.util.Date;
  50. import java.util.HashMap;
  51. import java.util.List;
  52. import java.util.Locale;
  53. import java.util.Map;
  54. import java.util.MissingResourceException;
  55. import java.util.ResourceBundle;
  56. import java.util.StringTokenizer;
  57. import java.util.regex.Matcher;
  58. import java.util.regex.Pattern;

  59. /**
  60.  * This class provides services for internationalization (i18n)
  61.  *
  62.  * @since v1.4.1
  63.  */
  64. public final class I18nService
  65. {
  66.     private static final String FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION = "fr.paris.lutece.portal.resources.{0}_messages";
  67.     private static final String FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.resources.{0}_messages";
  68.     private static final String FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.modules.{1}.resources.{1}_messages";
  69.     private static final Pattern PATTERN_LOCALIZED_KEY = Pattern.compile( "#i18n\\{(.*?)\\}" );
  70.     private static final String PROPERTY_AVAILABLES_LOCALES = "lutece.i18n.availableLocales";
  71.     private static final Locale LOCALE_DEFAULT = new Locale( "", "", "" );
  72.     private static final String PROPERTY_DEFAULT_LOCALE = "lutece.i18n.defaultLocale";
  73.     private static final String PROPERTY_FORMAT_DATE_SHORT_LIST = "lutece.format.date.short";
  74.     private static Map<String, String> _pluginBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
  75.     private static Map<String, String> _moduleBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
  76.     private static Map<String, String> _portalBundleNames = Collections.synchronizedMap( new HashMap<String, String>( ) );
  77.     private static final String PROPERTY_PATH_OVERRIDE = "path.i18n.override";
  78.     private static final ClassLoader _overrideLoader;
  79.     private static final Map<String, ResourceBundle> _resourceBundleCache = Collections.synchronizedMap( new HashMap<String, ResourceBundle>( ) );

  80.     static
  81.     {
  82.         File overridePath = null;

  83.         try
  84.         {
  85.             overridePath = new File( AppPathService.getPath( PROPERTY_PATH_OVERRIDE ) );
  86.         }
  87.         catch( AppException e )
  88.         {
  89.             // the key is unknown. Message override will be deactivated
  90.             AppLogService.error( "property {} is undefined. Message overriding will be disabled.", PROPERTY_PATH_OVERRIDE );
  91.         }

  92.         URL [ ] overrideURL = null;

  93.         if ( overridePath != null )
  94.         {
  95.             try
  96.             {
  97.                 overrideURL = new URL [ ] {
  98.                         overridePath.toURI( ).toURL( )
  99.                 };
  100.             }
  101.             catch( MalformedURLException e )
  102.             {
  103.                 AppLogService.error( "Error initializing message overriding: {}", e.getMessage( ), e );
  104.             }
  105.         }

  106.         if ( overrideURL != null )
  107.         {
  108.             _overrideLoader = new URLClassLoader( overrideURL, null );
  109.         }
  110.         else
  111.         {
  112.             _overrideLoader = null;
  113.         }
  114.     }

  115.     /**
  116.      * Private constructor
  117.      */
  118.     private I18nService( )
  119.     {
  120.     }

  121.     /**
  122.      * This method localize a string. It scans for localization keys and replace them by localized values.<br>
  123.      * The localization key structure is : #{bundle.key}.<br>
  124.      * bundle's values should be 'portal' or a plugin name.
  125.      *
  126.      * @param strSource
  127.      *            The string that contains localization keys
  128.      * @param locale
  129.      *            The locale
  130.      * @return The localized string
  131.      */
  132.     public static String localize( String strSource, Locale locale )
  133.     {
  134.         String result = strSource;

  135.         if ( strSource != null )
  136.         {
  137.             Matcher matcher = PATTERN_LOCALIZED_KEY.matcher( strSource );

  138.             if ( matcher.find( ) )
  139.             {
  140.                 StringBuffer sb = new StringBuffer( );

  141.                 do
  142.                 {
  143.                     matcher.appendReplacement( sb, getLocalizedString( matcher.group( 1 ), locale ) );
  144.                 }
  145.                 while ( matcher.find( ) );

  146.                 matcher.appendTail( sb );
  147.                 result = sb.toString( );
  148.             }
  149.         }

  150.         return result;
  151.     }

  152.     /**
  153.      * Returns the string corresponding to a given key for a given locale <br>
  154.      * <ul>
  155.      * <li>Core key structure :<br>
  156.      * <code>portal.[admin, features, insert, rbac, search, site, style, system, users, util].key</code></li>
  157.      * <li>Plugin key structure :<br>
  158.      * <code>[plugin].key </code></li>
  159.      * <li>Module key structure :<br>
  160.      * <code>module.[plugin].[module].key </code></li>
  161.      * </ul>
  162.      *
  163.      * @param strKey
  164.      *            The key of the string
  165.      * @param theLocale
  166.      *            The locale
  167.      * @return The string corresponding to the key
  168.      */
  169.     public static String getLocalizedString( String strKey, Locale theLocale )
  170.     {
  171.         Locale locale = theLocale;
  172.         String strReturn = "";

  173.         try
  174.         {
  175.             int nPos = strKey.indexOf( '.' );

  176.             if ( nPos != -1 )
  177.             {
  178.                 String strBundleKey = strKey.substring( 0, nPos );
  179.                 String strStringKey = strKey.substring( nPos + 1 );

  180.                 String strBundle;

  181.                 if ( !strBundleKey.equals( "portal" ) )
  182.                 {
  183.                     if ( strBundleKey.equals( "module" ) )
  184.                     {
  185.                         // module resource
  186.                         nPos = strStringKey.indexOf( '.' );

  187.                         String strPlugin = strStringKey.substring( 0, nPos );
  188.                         strStringKey = strStringKey.substring( nPos + 1 );
  189.                         nPos = strStringKey.indexOf( '.' );

  190.                         String strModule = strStringKey.substring( 0, nPos );
  191.                         strStringKey = strStringKey.substring( nPos + 1 );

  192.                         strBundle = getModuleBundleName( strPlugin, strModule );
  193.                     }
  194.                     else
  195.                     {
  196.                         // plugin resource
  197.                         strBundle = getPluginBundleName( strBundleKey );
  198.                     }
  199.                 }
  200.                 else
  201.                 {
  202.                     nPos = strStringKey.indexOf( '.' );

  203.                     String strElement = strStringKey.substring( 0, nPos );
  204.                     strStringKey = strStringKey.substring( nPos + 1 );

  205.                     strBundle = getPortalBundleName( strElement );
  206.                 }

  207.                 // if language is english use a special locale to force using default
  208.                 // bundle instead of the bundle of default locale.
  209.                 if ( locale.getLanguage( ).equals( Locale.ENGLISH.getLanguage( ) ) )
  210.                 {
  211.                     locale = LOCALE_DEFAULT;
  212.                 }

  213.                 ResourceBundle rbLabels = getResourceBundle( locale, strBundle );
  214.                 strReturn = rbLabels.getString( strStringKey );
  215.             }
  216.         }
  217.         catch( Exception e )
  218.         {
  219.             String strErrorMessage = "Error localizing key : '" + strKey + "' - " + e.getMessage( );

  220.             if ( e.getCause( ) != null )
  221.             {
  222.                 strErrorMessage += ( " - cause : " + e.getCause( ).getMessage( ) );
  223.             }

  224.             AppLogService.error( strErrorMessage );
  225.         }

  226.         return strReturn;
  227.     }

  228.     /**
  229.      * Get resource bundle name for plugin
  230.      *
  231.      * @param strBundleKey
  232.      *            the plugin key
  233.      * @return resource bundle name
  234.      */
  235.     private static String getPluginBundleName( String strBundleKey )
  236.     {
  237.         return _pluginBundleNames.computeIfAbsent( strBundleKey, s -> new MessageFormat( FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION ).format( new String [ ] {
  238.                 s
  239.         } ) );
  240.     }

  241.     /**
  242.      * Get resource bundle name for module
  243.      *
  244.      * @param strPlugin
  245.      *            the plugin key
  246.      * @param strModule
  247.      *            the module key
  248.      * @return resource bundle name
  249.      */
  250.     private static String getModuleBundleName( String strPlugin, String strModule )
  251.     {
  252.         String key = strPlugin + strModule;
  253.         return _moduleBundleNames.computeIfAbsent( key, s -> new MessageFormat( FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION ).format( new String [ ] {
  254.                 strPlugin, strModule
  255.         } ) );
  256.     }

  257.     /**
  258.      * Get resource bundle name for core element
  259.      *
  260.      * @param strElement
  261.      *            element name
  262.      * @return resource bundle name
  263.      */
  264.     private static String getPortalBundleName( String strElement )
  265.     {
  266.         return _portalBundleNames.computeIfAbsent( strElement, s -> new MessageFormat( FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION ).format( new String [ ] {
  267.                 s
  268.         } ) );
  269.     }

  270.     /**
  271.      * Returns the string corresponding to a given key for a given locale that use a MessageFormat pattern with arguments.
  272.      *
  273.      * @return The string corresponding to the key
  274.      * @param arguments
  275.      *            The arguments used as values by the formatter
  276.      * @param strKey
  277.      *            The key of the string that contains the pattern
  278.      * @param locale
  279.      *            The locale
  280.      */
  281.     public static String getLocalizedString( String strKey, Object [ ] arguments, Locale locale )
  282.     {
  283.         String strMessagePattern = getLocalizedString( strKey, locale );

  284.         return MessageFormat.format( strMessagePattern, arguments );
  285.     }

  286.     /**
  287.      * Format a date according to the given locale
  288.      *
  289.      * @param date
  290.      *            The date to format
  291.      * @param locale
  292.      *            The locale
  293.      * @param nDateFormat
  294.      *            A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
  295.      * @return The formatted date
  296.      */
  297.     public static String getLocalizedDate( Date date, Locale locale, int nDateFormat )
  298.     {
  299.         DateFormat dateFormatter = DateFormat.getDateInstance( nDateFormat, locale );
  300.         return dateFormatter.format( date );
  301.     }

  302.     /**
  303.      * Format a date according to the given locale
  304.      *
  305.      * @param date
  306.      *            The date to format
  307.      * @param locale
  308.      *            The locale
  309.      * @param nDateFormat
  310.      *            A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
  311.      * @param nTimeFormat
  312.      *            A TimeFormat constant corresponding to the expected format. (ie: DateFormat.SHORT)
  313.      * @return The formatted date
  314.      */
  315.     public static String getLocalizedDateTime( Date date, Locale locale, int nDateFormat, int nTimeFormat )
  316.     {
  317.         DateFormat dateFormatter = DateFormat.getDateTimeInstance( nDateFormat, nTimeFormat, locale );
  318.         return dateFormatter.format( date );
  319.     }

  320.     /**
  321.      * Returns supported locales for Lutece backoffice
  322.      *
  323.      * @return A list of locales
  324.      */
  325.     public static List<Locale> getAdminAvailableLocales( )
  326.     {
  327.         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_AVAILABLES_LOCALES );
  328.         StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
  329.         List<Locale> list = new ArrayList<>( );

  330.         while ( strTokens.hasMoreTokens( ) )
  331.         {
  332.             String strLanguage = strTokens.nextToken( );
  333.             Locale locale = new Locale( strLanguage );
  334.             list.add( locale );
  335.         }

  336.         return list;
  337.     }

  338.     /**
  339.      * Get the default Locale specified in properties file
  340.      *
  341.      * @return The default Locale
  342.      */
  343.     public static Locale getDefaultLocale( )
  344.     {
  345.         String strDefaultLocale = AppPropertiesService.getProperty( PROPERTY_DEFAULT_LOCALE );

  346.         return new Locale( strDefaultLocale );
  347.     }

  348.     /**
  349.      * Get the short date format specified by a locale
  350.      *
  351.      * @param locale
  352.      *            The locale
  353.      * @return The localized short date pattern or null else
  354.      */
  355.     public static String getDateFormatShortPattern( Locale locale )
  356.     {
  357.         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_FORMAT_DATE_SHORT_LIST );

  358.         if ( ( locale != null ) && ( strAvailableLocales != null ) && !strAvailableLocales.equals( "" ) )
  359.         {
  360.             StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
  361.             String strToken = null;

  362.             for ( Locale adminLocale : getAdminAvailableLocales( ) )
  363.             {
  364.                 if ( strTokens.hasMoreTokens( ) )
  365.                 {
  366.                     strToken = strTokens.nextToken( );
  367.                 }

  368.                 if ( adminLocale.getLanguage( ).equals( locale.getLanguage( ) ) )
  369.                 {
  370.                     return strToken;
  371.                 }
  372.             }
  373.         }

  374.         return null;
  375.     }

  376.     /**
  377.      * Returns a ReferenceList of available locales
  378.      *
  379.      * @param locale
  380.      *            The locale to display available languages
  381.      * @return A ReferenceList of available locales
  382.      */
  383.     public static ReferenceList getAdminLocales( Locale locale )
  384.     {
  385.         ReferenceList list = new ReferenceList( );

  386.         for ( Locale l : getAdminAvailableLocales( ) )
  387.         {
  388.             list.addItem( l.getLanguage( ), l.getDisplayLanguage( l ) );
  389.         }

  390.         return list;
  391.     }

  392.     /**
  393.      * Localize in place all items of a collection
  394.      *
  395.      * @param <E>
  396.      *            the type of the elements of the collection
  397.      *
  398.      * @param collection
  399.      *            The collection to localize
  400.      * @param locale
  401.      *            The locale
  402.      * @return The localized collection passed as argument
  403.      */
  404.     public static <E extends Localizable> Collection<E> localizeCollection( Collection<E> collection, Locale locale )
  405.     {
  406.         for ( Localizable object : collection )
  407.         {
  408.             object.setLocale( locale );
  409.         }

  410.         return collection;
  411.     }

  412.     /**
  413.      * Localize in place all items of a list
  414.      *
  415.      * @param <E>
  416.      *            the type of the elements of the list
  417.      *
  418.      * @param list
  419.      *            The list to localize
  420.      * @param locale
  421.      *            The locale
  422.      * @return The localized collection passed as argument
  423.      */
  424.     public static <E extends Localizable> List<E> localizeCollection( List<E> list, Locale locale )
  425.     {
  426.         for ( Localizable object : list )
  427.         {
  428.             object.setLocale( locale );
  429.         }

  430.         return list;
  431.     }

  432.     /**
  433.      * get the resource bundle, possibly with its override
  434.      *
  435.      * @param locale
  436.      *            the locale
  437.      * @param strBundle
  438.      *            the bundle name
  439.      * @return the resource bundle
  440.      */
  441.     private static ResourceBundle getResourceBundle( Locale locale, String strBundle )
  442.     {
  443.         String key = strBundle + locale.toString( );
  444.         return _resourceBundleCache.computeIfAbsent( key, k -> createResourceBundle( strBundle, locale ) );
  445.     }

  446.     private static ResourceBundle createResourceBundle( String strBundle, Locale locale )
  447.     {
  448.         ResourceBundle rbLabels = ResourceBundle.getBundle( strBundle, locale );

  449.         if ( _overrideLoader != null )
  450.         {
  451.             ResourceBundle overrideBundle = null;

  452.             try
  453.             {
  454.                 overrideBundle = ResourceBundle.getBundle( strBundle, locale, _overrideLoader );
  455.             }
  456.             catch( MissingResourceException e )
  457.             {
  458.                 // no override for this resource
  459.                 return rbLabels;
  460.             }

  461.             return new CombinedResourceBundle( overrideBundle, rbLabels );
  462.         }
  463.         return rbLabels;
  464.     }

  465.     /**
  466.      * Reset the caches
  467.      *
  468.      * @since 5.1
  469.      */
  470.     public static void resetCache( )
  471.     {
  472.         ResourceBundle.clearCache( );

  473.         if ( _overrideLoader != null )
  474.         {
  475.             ResourceBundle.clearCache( _overrideLoader );
  476.         }

  477.         _resourceBundleCache.clear( );
  478.     }
  479. }