使用 Spring Boot + Freemarker 开发 i18n 国际化应用

i18n(Internationalization),即国际化。目的是为了使软件、应用或网站能够适应不同的语言、地区,用户可以选择他们熟悉的语言进行交互,为所有用户提供一致的体验。

本文将会带你了解如何使用 Spring Boot + Freemarker 实现国际化。

假设我们有一个登录页面,其中有 2 个输入框,分别用于输入 用户名密码。对于使用不同语言的用户,需要显示不同的输入框名称。

创建项目

pom.xml 中添加 webfreemarker stater 依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

配置

定义国际化资源文件

通常我们会把项目中需要国际化的内容定义在不同的 properties 文件中,通过 properties 文件名的后缀来表示资源的 语言国别

resources 目录下创建 i18n 目录,用于存放国际化资源文件。接着,在 i18n 目录中创建如下 3 个 properties 文件,如下:

message.properties

# message.properties 的内容
# 空着就行

message_en_US.properties

# message_en_US.properties 的内容
# 英文
login.username=USERNAME
login.password=PASSWORD

message_zh_CN.properties

# message_zh_CN.properties 的内容
# 中文
login.username=用户名
login.password=密 码

如上,除了第 1 个空文件外,分别在 2 个资源文件中指定了不同语言的国际化内容。

  • message_en_US.properties 则表示英文资源,en 表示英文,US 表示美国。
  • message_zh_CN.properties 表示是中文资源,其中 zh 表示中文,CN 表示中国。

按照这个格式,你可以配置多个其他语言和国别的资源。

  • message_en_GB.properties:英国、英语
  • message_ko_KR.properties:韩文、韩国

也可以不指定国别,仅指定语言,如:message_en.properties,表示英语,但不分国别(美国、英国、加拿大都说英语)。

注意,properties 文件的编码一定要是 UTF-8!

message.properties 文件一定要存在,哪怕是空的。否则可能会在运行时遇到如下异常。

org.springframework.context.NoSuchMessageException: No message found under code '...' for locale '...'.

配置国际化

application.yaml 中添加如下配置:

spring:
  messages:
    # 指定资源文件夹以及文件的前缀
    basename: i18n/message
  
  freemarker:
    # 暴露 spring 提供的宏
    expose-spring-macro-helpers: true
    # 模板的加载路径
    template-loader-path: classpath:/templates/

spring.messages.basename 指定了国际化资源文件的 “前缀”,也就是上文国际化资源文件所在的目录和基本名称。

spring.freemarker 则是整合 Freemarker 模板引擎的配置。expose-spring-macro-helpers 设置为 true 暴露了 spring 提供的宏,我们会通过这个宏定义的指令来在模板上渲染国际化资源。template-loader-path 配置则是指定了模板引擎的目录(关于 Spring Boot 整合 Freemarker 的更多细节你可以参考 这篇文章)。

创建 LocaleResolver Bean

LocaleResolver 接口用于解析客户端的语言环境,默认它提供了如下实现:

  • CookieLocaleResolver:从 Cookie 解析客户端语言。
  • FixedLocaleResolver:固定使用 JVM 的默认语言,不支持更改。
  • SessionLocaleResolver:从 Session 解析客户端语言。

这里我们使用 CookieLocaleResolver,也就是说在用户选择了语言后,把语言信息存储在客户端浏览器的 Cookie 中,这样的话服务器就不用维护状态了。

创建 I18nConfiguration 配置类,如下:

package cn.springdoc.demo.configuration;

import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

@Configuration
public class I18nConfiguration {

    /**
     * LocaleResolver 用于解析客户端的语言环境
     * @return
     */
    @Bean
    public LocaleResolver localeResolver() {
        // 创建 CookieLocaleResolver,指定 cookie 名称为 _lang
        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver("_lang");
        // 设置默认的语言,国别。 Locale.CHINA 也就是 zn_CN
        cookieLocaleResolver.setDefaultLocale(Locale.CHINA);
        // 不允许 JS 读写此 Cookie
        cookieLocaleResolver.setCookieHttpOnly(true);
        // ... 其他设置
        return cookieLocaleResolver;
    }
}

通过 CookieLocaleResolver 构造函数指定 Cookie 的名称,默认是 org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE

setDefaultLocale 方法设置默认的语言。在客户端选择的语言不支持,或者客户端未选择语言的情况下,默认使用的国际化资源。这里设置为 Locale.CHINA,也就是 zh_CN

还可以设置关于 Cookie 的一些属性,如:sameSitemaxAgedomainpathsecure 等等。这里仅设置了 httpOnly 属性,表示禁止 JS 读写这个 Cookie。

创建 LocaleChangeInterceptor Bean

LocaleChangeInterceptor 拦截器,用于拦截用户的请求。根据用户请求参数来设置语言环境。

创建 MvcConfiguration 配置类,实现 WebMvcConfigurer 接口:

package cn.springdoc.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.localeChangeInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {

        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 受 GET 请求
        interceptor.setHttpMethods(HttpMethod.GET.name());
        // 传递客户端语言的查询参数
        interceptor.setParamName("_lang");
        // 忽略客户端非法的参数
        interceptor.setIgnoreInvalidLocale(true);

        return interceptor;
    }
}

创建 LocaleChangeInterceptor 拦截器实例 Bean。通过 setHttpMethods 方法指定要拦截的请求方法。setParamName 方法设置一个参数名称。客户端通过这个参数传递所选择的语言。setIgnoreInvalidLocale 方法表示是否要忽略客户端传递的非法参数值。

最后通过 addInterceptors 配置拦截器,拦截所有请求。

创建模板引擎

resources 目录下创建 templates 目录,用于存放模板引擎(上述配置 spring.freemarker.template-loader-path 定义的值)。

接着,在 templates 下创建 login.ftlh 模板引擎文件,内容如下。


<#-- 导入 spring 的宏 -->
<#import "/spring.ftl" as spring/>

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
        <form>
                <#-- 通过 spring.message 指令加载国际化资源 -->
                <@spring.message code='login.username'/> <input  name="username"/>
                <br/>
                <@spring.message code='login.password'/> <input  name="password" type="password"/>
        </form>
    </body>
</html>

首先在最顶部通过 <#import "/spring.ftl" as spring/> 导入 Spring 预定义的宏 /spring.ftl,命名为 spring。这个宏定义了大量的工具指令。

在模板中,通过 <@spring.message code='login.username'/> 调用 spring 宏中的 message 指令,其中 code 参数就是在国际化资源文件中定义的 KEY。该指令会根据当前请求所设置的语言环境,自动从对应语言的资源文件中根据 KEY 读取配置值。

最后,创建渲染 login 模板的 Controller:

package cn.springdoc.demo.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping
public class LoginController {

    @GetMapping("/login")
    public ModelAndView login () {
        return new ModelAndView("login");
    }
}

测试

启动应用后,用浏览器访问 http://localhost:8080/login

由于第一次访问,且未指定使用的语言。根据 LocaleResolver 配置,默认使用 zh_CN,所以你会看到中文表单名称。

中文登录页

接着,尝试添加 “语言参数”。访问 http://localhost:8080/login?_lang=en_US

这次,传递了一个 _lang=en_US 参数,LocaleChangeInterceptor 会拦截到此请求,并把当前请求环境设置为 en_US。所以,能看到英文表单名称。

_lang 参数的值要符合:[语言]_[国别]。格式

英文登录页

因为使用了 Cookie,所以只要你没有再次使用 _lang=en_US 参数来切换不同的语言,则所有请求都会被解析为 en_US 语言。

在代码中获取到国际化配置

除了模板引擎,我们也可以在代码中通过 MessageSource Bean 来根据 Local 获取不同的国际化消息值。

例如,在上述 LoginController 中注入 MessageSource

static final Logger log = LoggerFactory.getLogger(LoginController.class);

// 注入 MessageSource
@Autowired
private MessageSource messageSource;

@GetMapping("/login")
public ModelAndView login() {

    // 当前请求的语言环境
    Locale locale = LocaleContextHolder.getLocale();
    
    // 根据请求的语言环境来获取国际化配置信息
    String loginUserName = messageSource.getMessage("login.username", null, locale);

    log.info("Local={}, loginUserName={}", locale, loginUserName);

    return new ModelAndView("login");
}

首先,注入 MessageSource

在 Controller 方法中通过 LocaleContextHolder 获取到当前请求所设置的语言环境 Locale 对象。再使用 MessageSource 通过这个 Locale 对象获取对应的国际化配置信息。

修改后,重启应用。再次访问 http://localhost:8080/login,服务端输出日志如下:

INFO 8368 --- [nio-8080-exec-2] c.s.demo.web.controller.LoginController  : Local=en_US, loginUserName=USERNAME

如上,成功读取到了国际化的配置信息。因为上次请求使用的是 en_US,由于 Cookie 还在,所以这次请求的语言环境也同样是 en_US

总结

本文介绍了如何使用 Spring Boot + Freemarker 开发一个国际化应用。步骤总结如下:

  1. resources 目录下配置各个语言、国别的国际化资源文件。
  2. 在配置文件中设置国际化资源文件的目录和基本文件名称。
  3. 创建 LocaleResolver 实现,用于解析客户端选择的语言。
  4. 创建 LocaleChangeInterceptor 拦截器实现,并且指定要拦截的路径,用于客户端切换不同的语言。
  5. 在 Freemarker 中通过 <@spring.message code='login.username'/> 指令来渲染国际化内容。
  6. 在代码中通过 LocaleContextHolder 获取当前请求的语言环境,配合 MessageSource 就可以获取到国际化配置信息。