使用Velocity / FreeMarker模板进行电子邮件国际化

30

如何使用诸如Velocity或FreeMarker之类的模板引擎构建电子邮件正文以实现国际化?

通常人们倾向于创建这样的模板:

<h3>${message.hi} ${user.userName}, ${message.welcome}</h3>
<div>
   ${message.link}<a href="mailto:${user.emailAddress}">${user.emailAddress}</a>.
</div>

并且创建了一个资源束,其中包含诸如以下属性:

message.hi=Hi
message.welcome=Welcome to Spring!
message.link=Click here to send email.

这会带来一个基本问题:如果我的 .vm 文件变得很大,有很多行文本,那么在单独的资源包 (.properties) 文件中翻译和管理每个文件将变得乏味。

我想要做的是,为每种语言创建一个单独的 .vm 文件,类似于 mytemplate_en_gb.vm、mytemplate_fr_fr.vm、mytemplate_de_de.vm,然后以某种方式告诉 Velocity/Spring 根据输入的区域设置选择正确的文件。

这在 Spring 中是否可行?或者我应该寻找更简单和明显的替代方法吗?

注意:我已经看过关于如何使用模板引擎创建电子邮件正文的Spring教程。但它似乎没有回答我的国际化问题。

2个回答

42

使用一个模板和多个language.properties文件比使用多个模板更好。

这会带来一个基本问题:如果我的.vm文件变得很大,包含了许多文本行,那么在单独的资源束(.properties)文件中翻译和管理每个文件将变得繁琐。

如果您的电子邮件结构在多个 .vm 文件上重复,维护起来会更加困难。此外,您还需要重新发明资源束的回退机制。资源束试图在给定区域设置的情况下找到最接近的匹配项。例如,如果区域设置是 en_GB,则它会按顺序查找以下文件,如果没有可用的文件,则回退到最后一个文件。

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

我将在此处发布(详细说明)简化 Velocity 模板中读取资源束所需执行的操作。

在Velocity模板中访问资源束

Spring配置

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

模板助手类

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模板

#parse("init.vm")
#msg("email.hello") ${user} / $user,
#msgArgs("email.message", [${emailId}]).
<h1>#msg("email.heading")</h1>

我不得不创建一个简写宏 msg 来从消息包中读取信息。它看起来像这样:

#**
 * 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

资源包

email.hello=Hello
email.heading=This is a localised message
email.message=your email id : {0} got updated in our system.

用法

Map<String, Object> data = new HashMap<String, Object>();
data.put("user", "Adarsh");
data.put("emailId", "adarsh@email.com");

String body = templateHelper.merge("send-email.vm", data, locale);

2
这是一篇非常有用的文章,然而,能够像这样传递模板值到国际化消息中会更好:email.hello=你好 {0},#parse("init.vm") #msg("email.hello" $user.name)目前似乎不可能实现,虽然我不知道为什么。 - Ken
@Ken 谢谢你,你帮我省了不少时间。 - Mitul Gedeeya
也许有人会遇到同样的问题:如果你需要向getMessage发送多个参数,可以使用$messages.getMessage($key,$parameters.toArray($arraySample),$locale),其中$arraySample已在数据映射中设置:data.put("arraySample", new Object[0])。我不知道这是否是最佳解决方案,但它能发挥作用,因为默认情况下,Velocity会发送ArrayList而不是数组。 - delucasvb

21
这是Freemarker的解决方案(一个模板,多个资源文件)。 主程序
// 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类

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模板

${msg("subject.title")}

谢谢。如果要在模型级别应用您的解决方案,请在@ ControllerAdvice-d类中放置以下内容: @Autowired private MessageResolverMethod mrm; @ModelAttribute(“msg”) public FormatDateTimeMethodModel formatDateTime(){ 返回mrm; } - Oleksii Kyslytsyn
哇,谢谢!终于用这段代码将我的模板减少到了一个。 - mwarren
请注意,TemplateMethodModel现已过时,建议改用TemplateMethodModelEx。此外,我不得不通过toString()方法将参数转换为字符串,而不是使用显式的(String)转换。 - PetarMI

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接