How can I achieve i18n using a templating engine such as Velocity or FreeMarker for constructing email body?
Typically people tend to create templates like:
<h3>${message.hi} ${user.userName}, ${message.welcome}</h3> <div> ${message.link}<a href="mailto:${user.emailAddress}">${user.emailAddress}</a>. </div>
And have a resource bundle created with properties like:
message.hi=Hi message.welcome=Welcome to Spring! message.link=Click here to send email.
This creates one basic problem: If my .vm
files becomes large with many lines of text, it becomes tedious to translate and manage each of them in separate resource bundle (.properties
) files.
What I am trying to do is, have a separate .vm
file created for each language, something like mytemplate_en_gb.vm, mytemplate_fr_fr.vm, mytemplate_de_de.vm
and then somehow tell Velocity/Spring to pick up the right one based on the input Locale.
Is this possible in Spring? Or should I be looking at perhaps more simple and obvious alternative approaches?
Note: I have already seen the Spring tutorial on how to create email bodies using templating engines. But it doesn't seem to answer my question on i18n.
FreeMarker is a template engine, written in Java, and maintained by the Apache Foundation. We can use the FreeMarker Template Language, also known as FTL, to generate many text-based formats like web pages, email, or XML files.
An FTL file is a template used by FreeMarker, a Java template engine used to auto-generate text output. It contains source text as well as FreeMarker variable definitions and instructions that are used as placeholders for text substitutions. FTL files are commonly used for auto-generating HTML webpages, .
FreeMarker is a Java-based template engine which can be used in stand-alone or servlet-based Java programs. In FreeMarker you define templates, which are text files that contain the desired output, except that they contain placeholders like ${name} , and even some logic like conditionals, loops, etc.
Templates are written in the FreeMarker Template Language (FTL), which is a simple, specialized language (not a full-blown programming language like PHP). Usually, a general-purpose programming language (like Java) is used to prepare the data (issue database queries, do business calculations).
It turns out using one template and multiple language.properties files wins over having multiple templates.
This creates one basic problem: If my .vm files becomes large with many lines of text, it becomes tedious to translate and manage each of them in separate resource bundle (.properties) files.
It is even harder to maintain if your email structure is duplicated over multiple .vm
files. Also, one will have to re-invent the fall-back mechanism of resource bundles. Resource bundles try to find the nearest match given a locale. For example, if the locale is en_GB
, it tries to find the below files in order, falling back to the last one if none of them is available.
I will post (in detail) what I had to do to simplify reading resource bundles in Velocity templates here.
Spring Configuration
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="content/language" /> </bean> <bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean"> <property name="resourceLoaderPath" value="/WEB-INF/template/" /> <property name="velocityProperties"> <map> <entry key="velocimacro.library" value="/path/to/macro.vm" /> </map> </property> </bean> <bean id="templateHelper" class="com.foo.template.TemplateHelper"> <property name="velocityEngine" ref="velocityEngine" /> <property name="messageSource" ref="messageSource" /> </bean>
TemplateHelper Class
public class TemplateHelper { private static final XLogger logger = XLoggerFactory.getXLogger(TemplateHelper.class); private MessageSource messageSource; private VelocityEngine velocityEngine; public String merge(String templateLocation, Map<String, Object> data, Locale locale) { logger.entry(templateLocation, data, locale); if (data == null) { data = new HashMap<String, Object>(); } if (!data.containsKey("messages")) { data.put("messages", this.messageSource); } if (!data.containsKey("locale")) { data.put("locale", locale); } String text = VelocityEngineUtils.mergeTemplateIntoString(this.velocityEngine, templateLocation, data); logger.exit(text); return text; } }
Velocity Template
#parse("init.vm") #msg("email.hello") ${user} / $user, #msgArgs("email.message", [${emailId}]). <h1>#msg("email.heading")</h1>
I had to create a short-hand macro, msg
in order to read from message bundles. It looks like this:
#** * msg * * Shorthand macro to retrieve locale sensitive message from language.properties *# #macro(msg $key) $messages.getMessage($key,null,$locale) #end #macro(msgArgs $key, $args) $messages.getMessage($key,$args.toArray(),$locale) #end
Resource Bundle
email.hello=Hello email.heading=This is a localised message email.message=your email id : {0} got updated in our system.
Usage
Map<String, Object> data = new HashMap<String, Object>(); data.put("user", "Adarsh"); data.put("emailId", "[email protected]"); String body = templateHelper.merge("send-email.vm", data, locale);
Here's the solution (one template, several resource files) for Freemarker.
the main program
// defined in the Spring configuration file MessageSource messageSource; Configuration config = new Configuration(); // ... additional config settings // get the template (notice that there is no Locale involved here) Template template = config.getTemplate(templateName); Map<String, Object> model = new HashMap<String, Object>(); // the method called "msg" will be available inside the Freemarker template // this is where the locale comes into play model.put("msg", new MessageResolverMethod(messageSource, locale));
MessageResolverMethod class
private class MessageResolverMethod implements TemplateMethodModel { private MessageSource messageSource; private Locale locale; public MessageResolverMethod(MessageSource messageSource, Locale locale) { this.messageSource = messageSource; this.locale = locale; } @Override public Object exec(List arguments) throws TemplateModelException { if (arguments.size() != 1) { throw new TemplateModelException("Wrong number of arguments"); } String code = (String) arguments.get(0); if (code == null || code.isEmpty()) { throw new TemplateModelException("Invalid code value '" + code + "'"); } return messageSource.getMessage(code, null, locale); }
}
Freemarker template
${msg("subject.title")}
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