i18n(Internationalization),即国际化。目的是为了使软件、应用或网站能够适应不同的语言、地区,用户可以选择他们熟悉的语言进行交互,为所有用户提供一致的体验。
本文将会带你了解如何使用 Spring Boot + Freemarker 实现国际化。
假设我们有一个登录页面,其中有 2 个输入框,分别用于输入 用户名 和 密码。对于使用不同语言的用户,需要显示不同的输入框名称。
创建项目 {#创建项目}
在 pom.xml
中添加 web
和 freemarker
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-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 的一些属性,如:sameSite
、maxAge
、domain
、path
、secure
等等。这里仅设置了 httpOnly
属性,表示禁止 JS 读写这个 Cookie。
创建 LocaleChangeInterceptor Bean {#创建-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 开发一个国际化应用。步骤总结如下:
- 在
resources
目录下配置各个语言、国别的国际化资源文件。 - 在配置文件中设置国际化资源文件的目录和基本文件名称。
- 创建
LocaleResolver
实现,用于解析客户端选择的语言。 - 创建
LocaleChangeInterceptor
拦截器实现,并且指定要拦截的路径,用于客户端切换不同的语言。 - 在 Freemarker 中通过
<@spring.message code='login.username'/>
指令来渲染国际化内容。 - 在代码中通过
LocaleContextHolder
获取当前请求的语言环境,配合MessageSource
就可以获取到国际化配置信息。