阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

国际化

30次阅读
没有评论

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

@Primary
@Bean
LocaleResolver createLocaleResolver() {var clr = new CookieLocaleResolver();
    clr.setDefaultLocale(Locale.ENGLISH);
    clr.setDefaultTimeZone(TimeZone.getDefault());
    return clr;
}

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 内容如下:

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 如下:

@Bean("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 中:

@Component
public class MvcInterceptor implements HandlerInterceptor {@Autowired
    LocaleResolver localeResolver;

    // 注意注入的 MessageSource 名称是 i18n:
    @Autowired
    @Qualifier("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 的代码:

@Bean
ViewResolver createViewResolver(@Autowired ServletContext servletContext, @Autowired @Qualifier("i18n") MessageSource messageSource) {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() {@Override
        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 来实现该功能:

@Controller
public class LocaleController {final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    LocaleResolver localeResolver;

    @GetMapping("/locale/{lo}")
    public String setLocale(@PathVariable("lo") 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 应用程序通过 MessageSourceLocaleResolver,配合 View 实现国际化。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计5944字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中