在 Spring MVC 中动态生成可用语言列表

发布于 2024-10-24 01:21:24 字数 256 浏览 0 评论 0原文

我已经在 Spring MVC 3 中设置了 i18n,并且它工作正常。 有几个文件,每个文件都有自己的语言:messages_en.properties、messages_de.properties 等。

在我的一个 JSP 中,我需要向用户显示包含所有可用语言的组合,并且我希望此列表是动态的,即从服务器中现有的语言文件即时生成。

是否有任何内置方法可以生成此列表?或者我是否必须检查语言文件所在的文件夹并解析它们?

谢谢!

纳乔

I have set up i18n in Spring MVC 3, and it is working 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 the users a combo with all available languages, and I would like this list to be dynamic i.e. generated on the fly from the existing language files in the server.

Is there any built-in method to generate this list? Or do I have to resort to check the folder where the language files reside and parse them?

Thanks!

Nacho

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

说不完的你爱 2024-10-31 01:21:24

如何将其放入可以访问 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 值即可。

How about putting this into something that has access to the 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);
    }
}

Just make sure each supported language supplies a test.code value and you're done.

杀手六號 2024-10-31 01:21:24

想法如下:在 .properties 文件中创建 test 变量,该变量的内容必须是 ISO 国家名称(2 个符号,例如:Russia=RU)。对于默认区域设置,您可以设置 test=DEFAUL
文件夹结构和资源包内容:
输入图片此处描述
输入图片此处描述

{
    public static void main(String[] args) {
        Locale[] availableLocales = Locale.getAvailableLocales();
        List<Locale> existingLocales = new ArrayList<>();
        existingLocales.add(Locale.getDefault());

        for (Locale value : availableLocales) {
            if (isLocaleExist(value)) {
                existingLocales.add(value);
            }
        }

        for (Locale value : existingLocales) {
            System.out.println(value);
        }
    }

    public static boolean isLocaleExist(Locale locale) {
        ResourceBundle bundle = ResourceBundle.getBundle("languages.but", locale);
        final String value = bundle.getString("test");
        return value.equals(locale.getCountry());
    }
}

代码结果:
输入图片此处描述

Idea is following: create test variable in your .properties file and content of this variable must be ISO country name (2 symbols e.g: Russia=RU). For default locale you can set test=DEFAUL.
Folder structure and resource bundle content:
enter image description here
enter image description here

{
    public static void main(String[] args) {
        Locale[] availableLocales = Locale.getAvailableLocales();
        List<Locale> existingLocales = new ArrayList<>();
        existingLocales.add(Locale.getDefault());

        for (Locale value : availableLocales) {
            if (isLocaleExist(value)) {
                existingLocales.add(value);
            }
        }

        for (Locale value : existingLocales) {
            System.out.println(value);
        }
    }

    public static boolean isLocaleExist(Locale locale) {
        ResourceBundle bundle = ResourceBundle.getBundle("languages.but", locale);
        final String value = bundle.getString("test");
        return value.equals(locale.getCountry());
    }
}

Code result:
enter image description here

飘逸的'云 2024-10-31 01:21:24

这将是一个很好的功能,但我认为您不会找到内置方法,因为属性文件的“失败”机制意味着拥有 messages_de.properties 不会必然意味着每条消息都有德语版本。所以 Spring 无法构建一个很好的 Map 来从中获取密钥。

不过,您应该能够使用 Spring 来使您的工作变得更轻松,而不必自己访问文件系统:

Enumeration; allMsgs = bundleClassLoader.findResources("messages");

  • 然后迭代 Enumeration,获取语言环境(ende 等) ) 来自每个 URL 的部分

It would be a nice feature, but I don't think you'll find a built-in method because the "fall-through" mechanism of properties files means that having a messages_de.properties doesn't necessarily mean every message is available in German. So Spring can't build up a nice Map<Locale, ResourceBundle> from which you could obtain the keys.

You should be able to use Spring to make your job easier though, and not have to hit the filesystem yourself:

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

  • Then iterate over the Enumeration, getting the locale (en, de etc) part from each URL
叹沉浮 2024-10-31 01:21:24

好的,找到了两个解决方案。对于两者,假设它们在 Spring MVC @Controller 注解的类中执行。每个都会生成一个 HashMap (languages),其中键是 2 个字母的 ISO 语言代码,值是语言名称(在当前区域设置中,在这些示例中是一个名为 < code>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-messages:


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>

Ok, two solutions found. For both, assume they are being executed inside a Spring MVC @Controller-annotated class. Each will produce a HashMap (languages) in which the key is the 2-letter ISO language code, and the value the language name (in the current Locale, which in these examples is a static variable called HSConstants.currentLocale)

1.- The one submitted by @millhouse (see above/below), which works after a bit of tweaking:


    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);  

This solution requires that, in each of your language .properties files, you set an entry with the language (in the example above, it would be 'currentLanguage'). For ecample, in messages_it.properties, there must be an entry like this: currentLanguage=Italiano

2.- Raw method, i.e. accesing the folder/files directly: assuming the files languages are in /WEB-INF/languages, and have a basename of fr-messages:


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);  

And then, in your JSP, render the select box using the languages map:

<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>
も星光 2024-10-31 01:21:24

我想与大家分享我的解决方案。

当前问题的经过验证的响应(具有两个解决方案)确实很有趣。
第一个解决方案的唯一问题是使用硬编码消息键(“currentLanguage”),该键可能会从相应的属性文件中消失。
第二个需要对属性文件的基本名称(“fr-messages_”)进行硬编码。但文件名可以更改...

因此,我按照经过验证的响应的示例来扩展我的自定义 ResourceBundleMessageSource 来执行此操作。

最初,我需要获取 Spring 消息属性文件的内容(messages_en.propertiesmessages_fr.properties,...),因为我有一个完整的 Javascript 前端(使用 ExtJ)。因此,我需要在 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;
        }
    }
}

如果你想使用这两个功能(获取可用的 Locales 并从属性文件中获取所有 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>

然后,您只需将资源包注入到您想要获取可用Locale的类中:

@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;

这里是一个使用示例,用于在自动更新浏览器的浏览Locale之前检查Locale是否可用当 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's browsing locale. The new locale is null.");
            return;
        }
        CustomUserDetails userDetails = SecurityUtils.getCurrentUser();
        if(userDetails == null || userDetails.getUser() == null) {
            logger.debug("Cannot update the user's 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>

我希望这对您有用...

享受吧! :-)

I wish to share with you my solution.

The validated response (with the two solutions) of the current question is really interresting.
The only problem on the first solution is to use a hard coded message key ("currentLanguage") that can disappear from the corresponding properties file.
The second one needs to hard code the basename ("fr-messages_") of the properties file. But the file name can be changed...

So, I followed the example of the validated response to extend my custom ResourceBundleMessageSource to do that.

Initialy, I needed to get the content of the Spring message properties files (messages_en.properties, messages_fr.properties, ...) because I have a full Javascript front end (using ExtJs). So, I needed to load all the (internationalized) labels of the application on a JS object.
But it doesn't exist... For this reason, I have developed a custom ReloadableResourceBundleMessageSource class. The corresponding methods are "getAllProperties()", "getAllPropertiesAsMap()" and "getAllPropertiesAsMessages()".

Later, I needed to get the available Locales on the application. And reading this stackoverflow page, I had the idea to extend my ReloadableResourceBundleMessageSource class to do that. You can see the "getAvailableLocales()" and "isAvailableLocale()" (to test just one Locale) methods.

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;
        }
    }
}

If you want to use the two functionnalities (get the available Locales and get all Spring messages from the properties files), so you need to get this complete class.

To use this ReloadableResourceBundleMessageSource, it is really simple.
You need to declare the resource bundle :

<!-- 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>

Then, you just need to inject the resource bundle into the class where you want to get the available Locales:

@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;

Here is a usage example to check if the Locale is available before automatically update the browsing Locale of the User on database when the Spring LocaleChangeInterceptor detect a change (via URL for example => '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's browsing locale. The new locale is null.");
            return;
        }
        CustomUserDetails userDetails = SecurityUtils.getCurrentUser();
        if(userDetails == null || userDetails.getUser() == null) {
            logger.debug("Cannot update the user's 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);
        }
    }

}

The corresponding SessionLocaleResolver declaration:

<!-- 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>

I hope this will be useful to you...

Enjoy! :-)

情绪操控生活 2024-10-31 01:21:24

如果有人仍在寻找简洁的答案,希望这会有所帮助:

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

Hope this helps, if anyone is still looking for a concise answer:

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());
    }
}

The idea is to Autowire all available message sources messages_*.properties and derive the available locales depending on the filenames.
The default locale could be marked separately as supported like so:

availableLocals.add(Locale.getDefault()); // for default messages.properties
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文