国际化


在开发应用程序的时候,经常会遇到支持多语言的需求,这种支持多语言的功能称之为国际化,英文是internationalization,缩写为i18n(因为首字母i和末字母n中间有18个字母)。

还有针对特定地区的本地化功能,英文是localization,缩写为L10n,本地化是指根据地区调整类似姓名、日期的显示等。

也有把上面两者合称为全球化,英文是globalization,缩写为g11n。

在Java中,支持多语言和本地化是通过MessageFormat配合Locale实现的:

国际化 - 图1

对于Web应用程序,要实现国际化功能,主要是渲染View的时候,要把各种语言的资源文件提出来,这样,不同的用户访问同一个页面时,显示的语言就是不同的。

我们来看看在Spring MVC应用程序中如何实现国际化。

获取Locale

实现国际化的第一步是获取到用户的Locale。在Web应用程序中,HTTP规范规定了浏览器会在请求中携带Accept-Language头,用来指示用户浏览器设定的语言顺序,如:

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

  1. @Primary
  2. @Bean
  3. LocaleResolver createLocaleResolver() {
  4. var clr = new CookieLocaleResolver();
  5. clr.setDefaultLocale(Locale.ENGLISH);
  6. clr.setDefaultTimeZone(TimeZone.getDefault());
  7. return clr;
  8. }

CookieLocaleResolverHttpServletRequest中获取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内容如下:

  1. language.select=Language
  2. home=Home
  3. signin=Sign In
  4. copyright=Copyright©{0,number,#}

文件messages_zh_CN.properties内容如下:

  1. language.select=语言
  2. home=首页
  3. signin=登录
  4. copyright=版权所有©{0,number,#}

创建MessageSource

第三步是创建一个Spring提供的MessageSource实例,它自动读取所有的.properties文件,并提供一个统一接口来实现“翻译”:

  1. // code, arguments, locale:
  2. String text = messageSource.getMessage("signin", null, locale);

其中,signin是我们在.properties文件中定义的key,第二个参数是Object[]数组作为格式化时传入的参数,最后一个参数就是获取的用户Locale实例。

创建MessageSource如下:

  1. @Bean("i18n")
  2. MessageSource createMessageSource() {
  3. var messageSource = new ResourceBundleMessageSource();
  4. // 指定文件是UTF-8编码:
  5. messageSource.setDefaultEncoding("UTF-8");
  6. // 指定主文件名:
  7. messageSource.setBasename("messages");
  8. return messageSource;
  9. }

注意到ResourceBundleMessageSource会自动根据主文件名自动把所有相关语言的资源文件都读进来。

再注意到Spring容器会创建不只一个MessageSource实例,我们自己创建的这个MessageSource是专门给页面国际化使用的,因此命名为i18n,不会与其它MessageSource实例冲突。

实现多语言

要在View中使用MessageSource加上Locale输出多语言,我们通过编写一个MvcInterceptor,把相关资源注入到ModelAndView中:

  1. @Component
  2. public class MvcInterceptor implements HandlerInterceptor {
  3. @Autowired
  4. LocaleResolver localeResolver;
  5. // 注意注入的MessageSource名称是i18n:
  6. @Autowired
  7. @Qualifier("i18n")
  8. MessageSource messageSource;
  9. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  10. if (modelAndView != null) {
  11. // 解析用户的Locale:
  12. Locale locale = localeResolver.resolveLocale(request);
  13. // 放入Model:
  14. modelAndView.addObject("__messageSource__", messageSource);
  15. modelAndView.addObject("__locale__", locale);
  16. }
  17. }
  18. }

不要忘了在WebMvcConfigurer中注册MvcInterceptor。现在,就可以在View中调用MessageSource.getMessage()方法来实现多语言:

  1. <a href="/signin">{{ __messageSource__.getMessage('signin', null, __locale__) }}</a>

上述这种写法虽然可行,但格式太复杂了。使用View时,要根据每个特定的View引擎定制国际化函数。在Pebble中,我们可以封装一个国际化函数,名称就是下划线_,改造一下创建ViewResolver的代码:

  1. @Bean
  2. ViewResolver createViewResolver(@Autowired ServletContext servletContext, @Autowired @Qualifier("i18n") MessageSource messageSource) {
  3. var engine = new PebbleEngine.Builder()
  4. .autoEscaping(true)
  5. .cacheActive(false)
  6. .loader(new Servlet5Loader(servletContext))
  7. // 添加扩展:
  8. .extension(createExtension(messageSource))
  9. .build();
  10. var viewResolver = new PebbleViewResolver();
  11. viewResolver.setPrefix("/WEB-INF/templates/");
  12. viewResolver.setSuffix("");
  13. viewResolver.setPebbleEngine(engine);
  14. return viewResolver;
  15. }
  16. private Extension createExtension(MessageSource messageSource) {
  17. return new AbstractExtension() {
  18. @Override
  19. public Map<String, Function> getFunctions() {
  20. return Map.of("_", new Function() {
  21. public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
  22. String key = (String) args.get("0");
  23. List<Object> arguments = this.extractArguments(args);
  24. Locale locale = (Locale) context.getVariable("__locale__");
  25. return messageSource.getMessage(key, arguments.toArray(), "???" + key + "???", locale);
  26. }
  27. private List<Object> extractArguments(Map<String, Object> args) {
  28. int i = 1;
  29. List<Object> arguments = new ArrayList<>();
  30. while (args.containsKey(String.valueOf(i))) {
  31. Object param = args.get(String.valueOf(i));
  32. arguments.add(param);
  33. i++;
  34. }
  35. return arguments;
  36. }
  37. public List<String> getArgumentNames() {
  38. return null;
  39. }
  40. });
  41. }
  42. };
  43. }

这样,我们可以把多语言页面改写为:

  1. <a href="/signin">{{ _('signin') }}</a>

如果是带参数的多语言,需要把参数传进去:

  1. <h5>{{ _('copyright', 2020) }}</h5>

使用其它View引擎时,也应当根据引擎接口实现更方便的语法。

切换Locale

最后,我们需要允许用户手动切换Locale,编写一个LocaleController来实现该功能:

  1. @Controller
  2. public class LocaleController {
  3. final Logger logger = LoggerFactory.getLogger(getClass());
  4. @Autowired
  5. LocaleResolver localeResolver;
  6. @GetMapping("/locale/{lo}")
  7. public String setLocale(@PathVariable("lo") String lo, HttpServletRequest request, HttpServletResponse response) {
  8. // 根据传入的lo创建Locale实例:
  9. Locale locale = null;
  10. int pos = lo.indexOf('_');
  11. if (pos > 0) {
  12. String lang = lo.substring(0, pos);
  13. String country = lo.substring(pos + 1);
  14. locale = new Locale(lang, country);
  15. } else {
  16. locale = new Locale(lo);
  17. }
  18. // 设定此Locale:
  19. localeResolver.setLocale(request, response, locale);
  20. logger.info("locale is set to {}.", locale);
  21. // 刷新页面:
  22. String referer = request.getHeader("Referer");
  23. return "redirect:" + (referer == null ? "/" : referer);
  24. }
  25. }

在页面设计中,通常在右上角给用户提供一个语言选择列表,来看看效果:

i18n-en

切换到中文:

i18n-zh-cn

练习

国际化 - 图4下载练习:在MVC程序中实现国际化 (推荐使用IDE练习插件快速下载)

小结

多语言支持需要从HTTP请求中解析用户的Locale,然后针对不同Locale显示不同的语言;

Spring MVC应用程序通过MessageSourceLocaleResolver,配合View实现国际化。

读后有收获可以支付宝请作者喝咖啡:

国际化 - 图5