Spring - get all resolvable message keys

22,488

Solution 1

Well, I "solved" my problem by extending ResourceBundleMessageSource.

package org.springframework.context.support;

import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;

public class ExposedResourceBundleMessageSource extends
        ResourceBundleMessageSource {
    public Set<String> getKeys(String basename, Locale locale) {
        ResourceBundle bundle = getResourceBundle(basename, locale);
        return bundle.keySet();
    }
}

Now I have access to the keys, but I have to do an ugly cast in my controller, in addition to having to specify the message source basename. Ugh, that's a lot of coupling.

Set<String> keys = 
  ((ExposedResourceBundleMessageSource)
  messageSource).getKeys("messages", locale);

Solution 2

I have solve this issue.


public class ExposedResourceBundleMessageSource extends
        ResourceBundleMessageSource {
    public static final String WHOLE = "whole";
    private Set baseNames;
    private Map> cachedData = new HashMap>();

    public Set getKeys(String baseName, Locale locale) {
        ResourceBundle bundle = getResourceBundle(baseName, locale);
        return bundle.keySet();
    }

    public Map getKeyValues(String basename, Locale locale) {
        String cacheKey = basename + locale.getCountry();
        if (cachedData.containsKey(cacheKey)) {
            return cachedData.get(cacheKey);
        }
        ResourceBundle bundle = getResourceBundle(basename, locale);
        TreeMap treeMap = new TreeMap();
        for (String key : bundle.keySet()) {
            treeMap.put(key, getMessage(key, null, locale));
        }
        cachedData.put(cacheKey, treeMap);
        return treeMap;
    }

    public Map getKeyValues(Locale locale) {
        String cacheKey = WHOLE + locale.getCountry();
        if (cachedData.containsKey(cacheKey)) {
            return cachedData.get(cacheKey);
        }
        TreeMap treeMap = new TreeMap();
        for (String baseName : baseNames) {
            treeMap.putAll(getKeyValues(baseName, locale));
        }
        cachedData.put(cacheKey, treeMap);
        return treeMap;
    }

    public void setBasenames(String[] basenames) {
        baseNames = CollectionUtils.arrayAsSet(basenames);
        super.setBasenames(basenames);
    }


}

Solution 3

My solution:

  1. use spring <util:properties />

    <util:properties id="message" location="classpath:messages.properties" />
    
  2. Add an interceptor, with 'message' set to request attributes

    request.setAttribute("msgKeys", RequestContextUtils.getWebApplicationContext(request).getBean("message"));
    
  3. In JSP, use the msgKeys to retrieve message through <fmt:message /> tag

    var msg = {<c:forEach items="${msgKeys}" var="m" varStatus="s">
        <c:set var="key" value="${fn:substringBefore(m,'=')}"/>
        "${fn:substringAfter(key)}":"<fmt:message key="${key}"/>",
    </c:forEach>};
    

(Of course you need to further escape the output to match js string.)

So, for properties

app.name=Application Name
app.title=Some title

would output

var msg = { "name":"Application Name", "title":"Some title" };

The good thing about that is that you can do some more logic in c:forEach loop.

Solution 4

If you do not want to extend the ResourceBundleMessageSource class, you can do it like this:

@Component
class FrontendMessageLoader {
  import scala.collection.JavaConverters._

  @Resource(name = "messageSource")
  var properties: ResourceBundleMessageSource = _

  @PostConstruct def init(): Unit = {
    val locale = new Locale("en", "US")
    val baseNames: mutable.Set[String] = properties.getBasenameSet.asScala
    val messageMap: Map[String, String] =
      baseNames.flatMap { baseName =>
        readMessages(baseName, locale)
      }.toMap
    }
  }

  private def readMessages(baseName: String, locale: Locale) = {
    val bundle = ResourceBundle.getBundle(baseName, locale)
    bundle.getKeys.asScala.map(key => key -> bundle.getString(key))
  }
}

This is Scala but you get the idea and you can easily convert it to Java if needed. The point is that you get the base-names (which are the property files) and then load those files and then you can get the keys. I am passing also locale, but there are multiple overloaded versions for ResourceBundle.getBundle and you can choose the one that matches your needs.

Share:
22,488
Eero Heikkinen
Author by

Eero Heikkinen

Updated on May 25, 2020

Comments

  • Eero Heikkinen
    Eero Heikkinen almost 4 years

    I have a web application which uses Spring MVC. I would like to have my interface consist of just a single page which retrieves all data dynamically as JSON via AJAX. My problem is with internationalization. When I render content in jsps, I can use JSTL tags to resolve my keys (super-easy with Spring MVC):

    <fmt:message key="name"/>: ${name}
    <fmt:message key="title"/>: ${title}
    <fmt:message key="group"/>: ${group}
    

    When properly configured, it renders in finnish locale as

    Nimi: yay
    Otsikko: hoopla
    Ryhmä: doo
    

    Now, when I use json, I have only this coming in from the server:

    {
     name: "yay", 
     title: "hoopla",
     group: "doo"
    }
    

    There's no keynames! But I have to provide them somehow. I considered changing the keynames to their localized forms or adding the localized keynames to json output (eg. name_localized="Nimi") but both of these options feel like bad practice. I'm using jackson json to automatically parse my domain objects into json and I like the encapsulation it provides.

    The only feasible solution I came up with is this: dynamically create a javascript file with the localized keynames as variables.

    <script type="text/javascript">
    var name="Nimi";
    var title="Otsikko";
    var group="Ryhmä";
    </script>
    

    Once I have this loaded, I now have all the information in my javascript to handle the json! But there's a gotcha: my list of field names is dynamic. So actually static rendering in jsp would look like this:

    <c:forEach var="field" values="${fields}">
     <fmt:message key="${field.key}"/>: ${field.value}
    </c:forEach>
    

    I need to find all of the messages specified in my messages.properties. Spring MessageSource interface only supports retrieving messages by key. How can I get a list of keynames in my JSP which renders the localized javascript variables?

  • Patrick
    Patrick over 11 years
    Well, you can clear up a little bit of the ugliness by making a custom message source be the project's default. Hard to fit it all in just a comment, but <bean id="messageSource" class="com.yourproj.support.ExposedResourceBundleMessageSour‌​ce"> in your config and then @Autowired private ExposedResourceBundleMessageSourcemessageSource; in your class.
  • JBCP
    JBCP over 11 years
    This is a good solution because of the addition of caching and capturing the basenames automatically.
  • JBCP
    JBCP over 11 years
    However, using locale.getCountry() might be a problem if you support multiple languages within the same country. Just using locale itself is probably safer.
  • Adam Gent
    Adam Gent about 6 years
    For those looking for how you should get the Locale you probably want to use LocaleContextHolder.getLocale(). This will get the Locale bound to the thread which is typically what you want in a servlet/spring mvc environment.