Email Internationalization using Velocity/FreeMarker Templates

26,115

Solution 1

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.

  • language_en_GB.properties
  • language_en.properties
  • language.properties

I will post (in detail) what I had to do to simplify reading resource bundles in Velocity templates here.

Accessing Resource Bundle in a Velocity template

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

Solution 2

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")}
Share:
26,115
adarshr
Author by

adarshr

Updated on September 22, 2020

Comments

  • adarshr
    adarshr almost 4 years

    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.vmand 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.

  • Ken
    Ken over 10 years
    This is a really useful post, however, it would be good to be able to pass template values to the internationalization messages like this: email.hello=hello {0}, #parse("init.vm") #msg("email.hello" $user.name) at the moment this does not appear possible, though I can't see why.
  • Mitul Gedeeya
    Mitul Gedeeya over 9 years
    @Ken Thank you . you saved my time.
  • delucasvb
    delucasvb almost 8 years
    Maybe some one will have this same issue: if you have to send multiple parameters to getMessage, you could use $messages.getMessage($key, $parameters.toArray($arraySample), $locale) where $arraySample has been set in your data map: data.put("arraySample", new Object[0]); I don't know if this is the best solution, but it does the trick because by default, Velocity will send an ArrayList instead of an array
  • Oleksii Kyslytsyn
    Oleksii Kyslytsyn over 6 years
    Thanks. If you want to apply your solution on a model-level, inside @ ControllerAdvice-d class just put: @Autowired private MessageResolverMethod mrm; @ModelAttribute("msg") public FormatDateTimeMethodModel formatDateTime() { return mrm; }
  • mwarren
    mwarren over 5 years
    Wow, thanks ! Finally got to reduce my templates to one with this code.
  • PetarMI
    PetarMI over 4 years
    Note that the TemplateMethodModel is now deprecated and TemplateMethodModelEx is recommended instead. Additionally, I had to cast the argument to a String via the toString() method as opposed to using explicit (String) casting.