共计 5944 个字符,预计需要花费 15 分钟才能阅读完成。
在开发应用程序的时候,经常会遇到支持多语言的需求,这种支持多语言的功能称之为国际化,英文是 internationalization,缩写为 i18n(因为首字母 i 和末字母 n 中间有 18 个字母)。
还有针对特定地区的本地化功能,英文是 localization,缩写为 L10n,本地化是指根据地区调整类似姓名、日期的显示等。
也有把上面两者合称为全球化,英文是 globalization,缩写为 g11n。
在 Java 中,支持多语言和本地化是通过 MessageFormat
配合 Locale
实现的:
// MessageFormat | |
import java.text.MessageFormat; | |
import java.util.Locale; | |
public class Time {public static void main(String[] args) {double price = 123.5; | |
int number = 10; | |
Object[] arguments = { price, number}; | |
MessageFormat mfUS = new MessageFormat("Pay {0,number,currency} for {1} books.", Locale.US); | |
System.out.println(mfUS.format(arguments)); | |
MessageFormat mfZH = new MessageFormat("{1}本书一共{0,number,currency}。", Locale.CHINA); | |
System.out.println(mfZH.format(arguments)); | |
} | |
} |
对于 Web 应用程序,要实现国际化功能,主要是渲染 View 的时候,要把各种语言的资源文件提出来,这样,不同的用户访问同一个页面时,显示的语言就是不同的。
我们来看看在 Spring MVC 应用程序中如何实现国际化。
获取 Locale
实现国际化的第一步是获取到用户的 Locale
。在 Web 应用程序中,HTTP 规范规定了浏览器会在请求中携带Accept-Language
头,用来指示用户浏览器设定的语言顺序,如:
Accept-Language: zh-CN,zh;q=0.8,en;q=0.2
上述 HTTP 请求头表示优先选择简体中文,其次选择中文,最后选择英文。q
表示权重,解析后我们可获得一个根据优先级排序的语言列表,把它转换为 Java 的Locale
,即获得了用户的Locale
。大多数框架通常只返回权重最高的Locale
。
Spring MVC 通过 LocaleResolver
来自动从 HttpServletRequest
中获取 Locale
。有多种LocaleResolver
的实现类,其中最常用的是CookieLocaleResolver
:
LocaleResolver createLocaleResolver() {var clr = new CookieLocaleResolver(); | |
clr.setDefaultLocale(Locale.ENGLISH); | |
clr.setDefaultTimeZone(TimeZone.getDefault()); | |
return clr; | |
} |
CookieLocaleResolver
从 HttpServletRequest
中获取 Locale
时,首先根据一个特定的 Cookie 判断是否指定了Locale
,如果没有,就从 HTTP 头获取,如果还没有,就返回默认的Locale
。
当用户第一次访问网站时,CookieLocaleResolver
只能从 HTTP 头获取 Locale
,即使用浏览器的默认语言。通常网站也允许用户自己选择语言,此时,CookieLocaleResolver
就会把用户选择的语言存放到 Cookie 中,下一次访问时,就会返回用户上次选择的语言而不是浏览器默认语言。
提取资源文件
第二步是把写死在模板中的字符串以资源文件的方式存储在外部。对于多语言,主文件名如果命名为messages
,那么资源文件必须按如下方式命名并放入 classpath 中:
- 默认语言,文件名必须为
messages.properties
; - 简体中文,Locale 是
zh_CN
,文件名必须为messages_zh_CN.properties
; - 日文,Locale 是
ja_JP
,文件名必须为messages_ja_JP.properties
; - 其它更多语言……
每个资源文件都有相同的 key,例如,默认语言是英文,文件 messages.properties
内容如下:
language.select=Language | |
home=Home | |
signin=Sign In | |
copyright=Copyright©{0,number,#} |
文件 messages_zh_CN.properties
内容如下:
language.select= 语言 | |
home= 首页 | |
signin= 登录 | |
copyright= 版权所有©{0,number,#} |
创建 MessageSource
第三步是创建一个 Spring 提供的 MessageSource
实例,它自动读取所有的 .properties
文件,并提供一个统一接口来实现“翻译”:
// code, arguments, locale: | |
String text = messageSource.getMessage("signin", null, locale); |
其中,signin
是我们在 .properties
文件中定义的 key,第二个参数是 Object[]
数组作为格式化时传入的参数,最后一个参数就是获取的用户 Locale
实例。
创建 MessageSource
如下:
"i18n") | (|
MessageSource createMessageSource() {var messageSource = new ResourceBundleMessageSource(); | |
// 指定文件是 UTF- 8 编码: | |
messageSource.setDefaultEncoding("UTF-8"); | |
// 指定主文件名: | |
messageSource.setBasename("messages"); | |
return messageSource; | |
} |
注意到 ResourceBundleMessageSource
会自动根据主文件名自动把所有相关语言的资源文件都读进来。
再注意到 Spring 容器会创建不只一个 MessageSource
实例,我们自己创建的这个 MessageSource
是专门给页面国际化使用的,因此命名为 i18n
,不会与其它MessageSource
实例冲突。
实现多语言
要在 View 中使用 MessageSource
加上 Locale
输出多语言,我们通过编写一个 MvcInterceptor
,把相关资源注入到ModelAndView
中:
public class MvcInterceptor implements HandlerInterceptor { | |
LocaleResolver localeResolver; | |
// 注意注入的 MessageSource 名称是 i18n: | |
MessageSource messageSource; | |
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {if (modelAndView != null // 返回了 ModelAndView | |
&& modelAndView.getViewName() != null // 设置了 View | |
&& !modelAndView.getViewName().startsWith("redirect:") // 不是重定向 | |
) {// 解析用户的 Locale: | |
Locale locale = localeResolver.resolveLocale(request); | |
// 放入 Model: | |
modelAndView.addObject("__messageSource__", messageSource); | |
modelAndView.addObject("__locale__", locale); | |
} | |
} | |
} |
不要忘了在 WebMvcConfigurer
中注册 MvcInterceptor
。现在,就可以在 View 中调用MessageSource.getMessage()
方法来实现多语言:
<a href="/signin">{{__messageSource__.getMessage('signin', null, __locale__) }}</a>
上述这种写法虽然可行,但格式太复杂了。使用 View 时,要根据每个特定的 View 引擎定制国际化函数。在 Pebble 中,我们可以封装一个国际化函数,名称就是下划线 _
,改造一下创建ViewResolver
的代码:
ViewResolver createViewResolver("i18n") MessageSource messageSource) { ServletContext servletContext, (var engine = new PebbleEngine.Builder() | |
.autoEscaping(true) | |
.cacheActive(false) | |
.loader(new Servlet5Loader(servletContext)) | |
// 添加扩展: | |
.extension(createExtension(messageSource)) | |
.build(); | |
var viewResolver = new PebbleViewResolver(); | |
viewResolver.setPrefix("/WEB-INF/templates/"); | |
viewResolver.setSuffix(""); | |
viewResolver.setPebbleEngine(engine); | |
return viewResolver; | |
} | |
private Extension createExtension(MessageSource messageSource) {return new AbstractExtension() { | |
public Map<String, Function> getFunctions() {return Map.of("_", new Function() {public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {String key = (String) args.get("0"); | |
List<Object> arguments = this.extractArguments(args); | |
Locale locale = (Locale) context.getVariable("__locale__"); | |
return messageSource.getMessage(key, arguments.toArray(), "???" + key + "???", locale); | |
} | |
private List<Object> extractArguments(Map<String, Object> args) {int i = 1; | |
List<Object> arguments = new ArrayList<>(); | |
while (args.containsKey(String.valueOf(i))) {Object param = args.get(String.valueOf(i)); | |
arguments.add(param); | |
i++; | |
} | |
return arguments; | |
} | |
public List<String> getArgumentNames() {return null; | |
} | |
}); | |
} | |
}; | |
} |
这样,我们可以把多语言页面改写为:
<a href="/signin">{{_('signin') }}</a>
如果是带参数的多语言,需要把参数传进去:
<h5>{{_('copyright', 2020) }}</h5>
使用其它 View 引擎时,也应当根据引擎接口实现更方便的语法。
切换 Locale
最后,我们需要允许用户手动切换 Locale
,编写一个LocaleController
来实现该功能:
public class LocaleController {final Logger logger = LoggerFactory.getLogger(getClass()); | |
LocaleResolver localeResolver; | |
public String setLocale( { String lo, HttpServletRequest request, HttpServletResponse response)// 根据传入的 lo 创建 Locale 实例: | |
Locale locale = null; | |
int pos = lo.indexOf('_'); | |
if (pos > 0) {String lang = lo.substring(0, pos); | |
String country = lo.substring(pos + 1); | |
locale = new Locale(lang, country); | |
} else {locale = new Locale(lo); | |
} | |
// 设定此 Locale: | |
localeResolver.setLocale(request, response, locale); | |
logger.info("locale is set to {}.", locale); | |
// 刷新页面: | |
String referer = request.getHeader("Referer"); | |
return "redirect:" + (referer == null ? "/" : referer); | |
} | |
} |
在页面设计中,通常在右上角给用户提供一个语言选择列表,来看看效果:
切换到中文:
练习
在 Spring MVC 程序中实现国际化。
下载练习
小结
多语言支持需要从 HTTP 请求中解析用户的 Locale,然后针对不同 Locale 显示不同的语言;
Spring MVC 应用程序通过 MessageSource
和LocaleResolver
,配合 View 实现国际化。
