Dynamically create a list of available languages ​​in Spring MVC

I installed i18n in Spring MVC 3 and it works correctly. There are several files, each with its own language: messages_en.properties, messages_de.properties, etc.

In one of my JSPs, I need to show users combos with all available languages, and I would like this list to be dynamic, that is, generated on the fly from existing language files on the server.

Is there a built-in method to create this list? Or do I need to resort to checking the folder in which the language files are located and analyze them?

Thank!

Nacho

+4
source share
5 answers

, . , Spring MVC @Controller -annotated. HashMap (languages), 2- ISO ( Locale, , HSConstants.currentLocale)

1.- , @millhouse (. /), :


    HashMap languages = new HashMap();  
    final String defaultMessage = "NOT FOUND";  
    HashMap availableLocales = new HashMap();  
    for (Locale locale : Locale.getAvailableLocales()) {  
        String msg = rrbms.getMessage("currentLanguage", null, defaultMessage, locale);  
        if (!defaultMessage.equals(msg) && !availableLocales.containsKey(locale.getLanguage())){  
            availableLocales.put(locale.getLanguage(), locale);  
        }  
    }  
    for (String c : availableLocales.keySet()){  
        languages.put(c, availableLocales.get(c).getDisplayLanguage(HSConstants.currentLocale));  
    }  
    model.addAttribute("languages", languages);  

, .properties ​​ ( "currentLanguage" ). , messages_it.properties, : currentLanguage = Italiano

2.- , / : /WEB -INF/languages ​​ fr-:


HashMap languages = new HashMap();  
String languagesFolderPath = request.getSession().getServletContext().getRealPath("/WEB-INF/languages");  
File folder = new File(languagesFolderPath);  
File[] listOfFiles = folder.listFiles();  

for (int i = 0; i < listOfFiles.length; i++){  
   String fileName = listOfFiles[i].getName();  
   if (fileName.startsWith("fr-messages_") && fileName.endsWith(".properties")){  
      // Extract the language code, which is between the underscore and the .properties extension  
      String language = fileName.substring(12, fileName.indexOf(".properties"));  
      Locale l = new Locale(language);  
      languages.put(language, l.getDisplayLanguage(HSConstants.currentLocale));  
   }  
}  
model.addAttribute("languages", languages);  

, JSP, languages:

<select name="language">
    <c:forEach items="${languages}" var="language">
        <c:choose>
            <c:when test="${platform.language == language.key}">
                <option value="${language.key}" selected="SELECTED">${language.value}</option>
            </c:when>
            <c:otherwise>
                <option value="${language.key}">${language.value}</option>
            </c:otherwise>
        </c:choose>                         
    </c:forEach>
</select>
0

, , ReloadableResourceBundleMessageSource?

 
ReloadableResourceBundleMessageSource rrbms = getMessageSource();   
final String defaultMessage = "NOT FOUND";
List<Locale> availableLocales = new ArrayList<Locale>();
for (Locale locale : Locale.getAvailableLocales()) {
    String msg = rrbms.getMessage("test.code", null, defaultMessage, locale);
    if (!defaultMessage.equals(msg)) {
       availableLocales.add(locale);
    }
}

, test.code, .

+1

, , , "" , message_de.properties . , Spring Map<Locale, ResourceBundle>, .

Spring, , :

Enumeration<URL> allMsgs = bundleClassLoader.findResources("messages");

  • Enumeration, locale (en, de ..) URL
0

.

( ) . ( "currentLanguage" ), . ( "fr-messages_" ) . ...

, , ResourceBundleMessageSource, .

Spring (messages_en.properties, messages_fr.properties,...), Javascript ( ExtJs). , () JS. ... ReloadableResourceBundleMessageSource. "getAllProperties()", "getAllPropertiesAsMap()" "getAllPropertiesAsMessages()".

. , stackoverflow, ReloadableResourceBundleMessageSource, . "getAvailableLocales()" "isAvailableLocale()" ( ).

package fr.ina.archibald.web.support;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ReflectionUtils;

import fr.ina.archibald.commons.util.StringUtils;
import fr.ina.archibald.entity.MessageEntity;

/**
 * Custom {@link org.springframework.context.support.ReloadableResourceBundleMessageSource}.
 * 
 * @author srambeau
 */
public class ReloadableResourceBundleMessageSource extends org.springframework.context.support.ReloadableResourceBundleMessageSource {

    private static final Logger LOGGER = LoggerFactory.getLogger(ReloadableResourceBundleMessageSource.class);

    private static final String PROPERTIES_SUFFIX = ".properties";

    private static final String XML_SUFFIX = ".xml";

    private Set<Locale> cacheAvailableLocales;

    private Set<Resource> cacheResources;

    /**
     * Returns all messages for the specified {@code Locale}.
     * 
     * @param locale the {@code Locale}.
     * 
     * @return a {@code Properties} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
     */
    public Properties getAllProperties(final Locale locale) {
        if(locale == null) {
            LOGGER.debug("Cannot get all properties. 'locale' argument is null.");
            return null;
        }
        return getMergedProperties(locale).getProperties();
    }

    /**
     * Returns all messages for the specified {@code Locale}.
     * 
     * @param locale the {@code Locale}.
     * 
     * @return a {@code Map} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Map<String, String> getAllPropertiesAsMap(final Locale locale) {
        if(locale == null) {
            LOGGER.debug("Cannot get all properties as Map. 'locale' argument is null.");
            return null;
        }
        Properties props = getAllProperties(locale);
        if(props == null) {
            LOGGER.debug("Cannot get all properties as Map. The properties are missing.");
            return null;
        }
        return new HashMap<String, String>((Map) props);
    }

    /**
     * Returns all messages for the specified {@code Locale}.
     * 
     * @param locale the {@code Locale}.
     * 
     * @return a {@code List<MessageEntity>} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
     */
    public List<MessageEntity> getAllPropertiesAsMessages(final Locale locale) {
        if(locale == null) {
            LOGGER.debug("Cannot get all properties as MessageEntity. 'locale' argument is null.");
            return null;
        }
        Properties props = getAllProperties(locale);
        if(props == null) {
            LOGGER.debug("Cannot get all properties as MessageEntity. The properties are missing.");
            return null;
        }
        Set<Entry<Object, Object>> propsSet = props.entrySet();
        List<MessageEntity> messages = new ArrayList<MessageEntity>();
        for(Entry<Object, Object> prop : propsSet) {
            messages.add(new MessageEntity((String) prop.getKey(), (String) prop.getValue()));
        }
        return messages;
    }

    /**
     * Returns the available {@code Locales} on the specified application context. Calculated from the Spring message files of the application context.
     * <p>
     * Example of Locales returned corresponding with the messages files defines on the application:
     * 
     * <pre>
     * messages_en.properties                         --> en
     * messages_fr.properties                         --> fr
     * messages_en.properties, messages_fr.properties --> en, fr
     * </pre>
     * </p>
     * 
     * @return the set of {@code Locales} or null if an error occurs.
     */
    public Set<Locale> getAvailableLocales() {
        if(cacheAvailableLocales != null) {
            return cacheAvailableLocales;
        }
        cacheAvailableLocales = getLocales(getAllFileNames(), getMessageFilePrefixes());
        return cacheAvailableLocales;
    }

    /**
     * Indicates if the specified {@code Locale} is available on the application.
     * <p>
     * Examples of results returned if the application contains the files "messages_en.properties" and "messages_fr.properties":
     * 
     * <pre>
     * en --> true
     * fr --> true
     * de --> false
     * es --> false
     * </pre>
     * 
     * @param locale the {@code Locale}.
     * 
     * @return {@code true} if the locale is available, {@code false} otherwise.
     */
    public boolean isAvailableLocale(final Locale locale) {
        Set<Locale> locales = getAvailableLocales();
        if(locales == null) {
            return false;
        }
        return locales.contains(locale);
    }

    // ********************** PRIVATE METHODES **********************

    /**
     * Returns the {@code Locales} specified on the file names.
     * 
     * @param fileNames the file names.
     * @param filePrefixes the basenames' prefixes of the resources bundles.
     * 
     * @return the set of the {@code Locales}.
     */
    private Set<Locale> getLocales(final List<String> fileNames, List<String> filePrefixes) {
        if(fileNames == null || fileNames.isEmpty() || filePrefixes == null || filePrefixes.isEmpty()) {
            LOGGER.debug("Cannot get available Locales. fileNames=[" + StringUtils.toString(fileNames) + "], filePrefixes=[" + StringUtils.toString(filePrefixes) + "]");
            return null;
        }
        Set<Locale> locales = new HashSet<Locale>();
        for(String fileName : fileNames) {
            String fileNameWithoutExtension = FilenameUtils.getBaseName(fileName);
            for(String filePrefixe : filePrefixes) {
                String localeStr = fileNameWithoutExtension.substring(filePrefixe.length() + 1);
                try {
                    locales.add(LocaleUtils.toLocale(localeStr));
                } catch(IllegalArgumentException ex) {
                    continue;
                }
            }
        }
        return locales;
    }

    /**
     * Returns all the file names of the resources bundles.
     * 
     * @return the list of file names or {@code null} if the resources are missing.
     */
    private List<String> getAllFileNames() {
        Set<Resource> resources = getAllResources();
        if(resources == null) {
            LOGGER.debug("Missing resources bundles.");
            return null;
        }
        List<String> filenames = new ArrayList<String>(resources.size());
        for(Resource resource : resources) {
            filenames.add(resource.getFilename());
        }
        return filenames;
    }

    /**
     * Gets the array of the prefixes for messages files.
     * 
     * <pre>
     * "WEB-INF/messages"               --> "messages"
     * "classpath:config/i18n/messages" --> "messages"
     * "messages"                       --> "messages"
     * </pre>
     * 
     * @return the array of the prefixes or null if an error occurs.
     */
    private List<String> getMessageFilePrefixes() {
        String[] basenames = getBasenames();
        if(basenames == null) {
            LOGGER.debug("Missing basenames of the resources bundles.");
            return null;
        }
        List<String> prefixes = new ArrayList<String>(basenames.length);
        for(int i = 0; i < basenames.length; ++i) {
            prefixes.add(FilenameUtils.getName(basenames[i]));
        }
        return prefixes;
    }

    /**
     * Returns all the resources bundles.
     * 
     * @return the set of resources or null if {@code basenames} or the {@link ResourceLoader} is missing.
     */
    private Set<Resource> getAllResources() {
        if(cacheResources != null) {
            return cacheResources;
        }
        String[] basenames = getBasenames();
        if(basenames == null) {
            LOGGER.debug("Missing basenames of the resources bundles.");
            return null;
        }
        ResourceLoader resourceLoader = getResourceLoader();
        if(resourceLoader == null) {
            LOGGER.debug("Missing ResourceLoader.");
            return null;
        }

        Set<Resource> resources = new HashSet<Resource>();
        for(String basename : basenames) {
            for(Locale locale : Locale.getAvailableLocales()) {
                List<String> filenames = calculateFilenamesForLocale(basename, locale);
                for(String filename : filenames) {
                    Resource resource = resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
                    if( ! resource.exists()) {
                        resource = resourceLoader.getResource(filename + XML_SUFFIX);
                    }
                    if(resource.exists()) {
                        resources.add(resource);
                    }
                }
            }
        }
        cacheResources = resources;
        return resources;
    }

    /**
     * Gets the array of basenames, each following the basic ResourceBundle convention of not specifying file extension or language codes.
     * 
     * @return the array of basenames or null if an error occurs.
     * 
     * @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setBasenames
     */
    private String[] getBasenames() {
        Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "basenames");
        if(field == null) {
            LOGGER.debug("Missing field 'basenames' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
            return null;
        }
        ReflectionUtils.makeAccessible(field);
        try {
            return (String[]) field.get(this);
        } catch(Exception ex) {
            LOGGER.debug("Unable to get the 'basenames' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
            return null;
        }
    }

    /**
     * Gets the resource loader.
     * 
     * @return the resource loader.
     * 
     * @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setResourceLoader
     */
    private ResourceLoader getResourceLoader() {
        Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "resourceLoader");
        if(field == null) {
            LOGGER.debug("Missing field 'resourceLoader' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
            return null;
        }
        ReflectionUtils.makeAccessible(field);
        try {
            return (ResourceLoader) field.get(this);
        } catch(Exception ex) {
            LOGGER.debug("Unable to get the 'resourceLoader' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
            return null;
        }
    }
}

( Spring ), .

ReloadableResourceBundleMessageSource, . :

<!-- Custom message source. -->
<bean id="messageSource" class="fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:config/i18n/messages" />
    <property name="defaultEncoding" value="UTF-8" />
</bean>

, Locales:

@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;

, , Locale Locale of the User , Spring LocaleChangeInterceptor ( URL = > 'http://your.domain?lang=en '):

package fr.ina.archibald.web.resolver;

import java.util.Locale;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;

import fr.ina.archibald.commons.annotation.Log;
import fr.ina.archibald.dao.entity.UserEntity;
import fr.ina.archibald.security.entity.CustomUserDetails;
import fr.ina.archibald.security.util.SecurityUtils;
import fr.ina.archibald.service.UserService;
import fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource;

/**
 * Custom SessionLocaleResolver.
 * 
 * @author srambeau
 * 
 * @see org.springframework.web.servlet.i18n.SessionLocaleResolver
 */
public class SessionLocaleResolver extends org.springframework.web.servlet.i18n.SessionLocaleResolver {

    @Log
    private Logger logger;

    @Inject
    private UserService userService;

    @Inject
    private ReloadableResourceBundleMessageSource resourceBundleMessageSource;

    @Override
    public void setLocale(HttpServletRequest req, HttpServletResponse res, Locale newLocale) {
        super.setLocale(req, res, newLocale);
        updateUserLocale(newLocale);
    }

    // /**
    // * Returns the default Locale that this resolver is supposed to fall back to, if any.
    // */
    // @Override
    // public Locale getDefaultLocale() {
    // return super.getDefaultLocale();
    // }

    // ********************** PRIVATE METHODES **********************

    /**
     * Updates the locale of the currently logged in user with the new Locale.
     * <p>
     * The locale is not updated if the specified locale is {@code null} or the same as the previous, if the user is missing or if an error occurs.
     * </p>
     * 
     * @param newLocale the new locale.
     */
    private void updateUserLocale(final Locale newLocale) {
        if(newLocale == null) {
            logger.debug("Cannot update the user browsing locale. The new locale is null.");
            return;
        }
        CustomUserDetails userDetails = SecurityUtils.getCurrentUser();
        if(userDetails == null || userDetails.getUser() == null) {
            logger.debug("Cannot update the user browsing locale. The user is missing.");
            return;
        }
        UserEntity user = userDetails.getUser();
        // Updates the user locale if and only if the locale has changed and is available on the application.
        if(newLocale.equals(user.getBrowsingLocale()) || ! resourceBundleMessageSource.isAvailableLocale(newLocale)) {
            return;
        }
        user.setBrowsingLocale(newLocale);
        try {
            userService.update(user);
        } catch(Exception ex) {
            logger.error("The browsing locale of the user with identifier " + user.getUserId() + " cannot be updated.", ex);
        }
    }

}

SessionLocaleResolver:

<!-- This custom SessionLocaleResolver allows to update the user Locale when it change. -->
<bean id="localeResolver" class="fr.ina.archibald.web.resolver.SessionLocaleResolver">
    <property name="defaultLocale" value="fr" />
</bean>

, ...

!: -)

0

, , - :

import org.springframework.core.io.Resource;

@Configuration
class LanguageConfig {

    private final Set<Locale> availableLocals;

    public LanguageConfig(@Value("classpath*:messages_*.properties") final Resource[] localesResources) {
        availableLocals = getAvailableLocalesFromResources(localesResources);
    }

    private Set<Locale> getAvailableLocalesFromResources(Resource[] localesResources) {
        return Arrays.stream(localesResources).map(resource -> {
            final String localeCode = resource.getFilename().split("messages_")[1].split(".properties")[0];
            return Locale.forLanguageTag(localeCode);
        }).collect(Collectors.toSet());
    }
}

, Autowire messages_*.properties . :

availableLocals.add(Locale.getDefault()); // for default messages.properties
0

All Articles