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?
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);
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);
}
}
My solution:
use spring <util:properties />
<util:properties id="message" location="classpath:messages.properties" />
Add an interceptor, with 'message' set to request attributes
request.setAttribute("msgKeys", RequestContextUtils.getWebApplicationContext(request).getBean("message"));
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With