Java国际化
Java使用Unicode来处理所有字符。
Locales
国际化主要涉及的是数字、日期、金额等。
有若干个专门负责格式处理的类。为了对格式进行控制,可以使用Locale类。它描述了:
- 一种语言
- 一个位置(通常包含)
- 一段脚本(可选,自Java SE7开始支持)
- 一个变体(可选)
- 指定诸如方言或拼写规则之类的杂项。
Locale对象示例
language=English,location=United States
language=German,location=Germany #货币表示为欧元
#瑞士有四种官方语言,一个说德语的瑞士人使用:货币会被表示成瑞士法郎。
language=German,location=Switzerland
如果只设定了语言,则不能处理和国家相关的问题。
language=German
为了以一种简练而标准的方式来表达语言和位置,Java语言使用ISO所定义的编码。本地语言由小写的2个字母来代替,它遵循ISO-639-1,国家代码由大写的2个字母的代码来表示,它遵循ISO-3166-1,
只要提供了语言代码,或者语言与国家代码,就可以构造Locale对象了。
Locale german=new Locale("de")
Locale germanGermany=new Locale("de","DE")
locale germanSwitzerland = new Locale("de","CH")
JAVA SE定义了大量Locale对象和语言Locale,但是没有设定位置。
//获取默认Locale
Locale locale = Locale.getDefault()
对于所有依赖Locale的类,可以返回一个他们所支持的Locale列表。
Locale[] l = DataFormat.getAvailableLocales();
参考
数字格式
数字和货币是高度依赖locale的,Java提供了一个格式器(formatter)对象的集合,可以对java.txt包中的数字值进行格式化和解析。
相关类:
- NumberFormat
参考
货币标识(ISO 4217):https://www.iban.hk/currency-codes
日期和时间
当格式化日期和时间时,需要考虑4个与Locale相关的问题:
- 月份和星期
- 年月日的顺序
- 公历可能不是首选的日期表示法
- 时区
相关类:
- DataFormat
排序
Java语言中,String类的CompareTo()方式是用Unicode字符来决定顺序的。
如果需要根据Locale排序,则先获取一个Locale对象,然后获取**Collator
**对象,再比较。
Locale loc = new Locale("de","DE");
Collator col = Collator.getInstance(loc);
if (col.Compare(a,b) < 0)
{
... ...
}
排序强度
可以设置排序器的强度来选择不同的排序行为。字符间的差别可以分为:首要的(Primary),其次的(secondary),和再次的(tertiary)。
**Collator
**定义了对应的枚举值
分解
偶尔一个字符或字符序列在描述成Unicode时,可以有多种方式。Unicode标准对字符串定义了四种范化形式:D,KD,C,KC。参考:https://www.unicode.org/reports/tr15/tr15-23.html
消息格式化
MessageFormat
类,用来格式化带变量的文本,
"On {2},a {0} destroyed {1} houses and cuased {3} of damage"
占位符索引后可以跟一个类型和一个风格,他们之间用逗号隔开。
类型可以是:
- number
- time
- date
- choice
如果类型是number,则风格可以是:
- integer
- currency
- precent
参考:DecimalFormat
类,SimpleDateFormat
类。
静态的MessageFormat.format()方法使用当前Locale对值进行格式化,如果要指定Locale,则:
MessageFormat mf= new MessageFormat(pattern,loc); String msg= mf.format(new Object[]{values});
选择格式
选择格式可以用来根据不同语言,使用特殊的语法,例如a,an等
choice格式化选项就是为了这个目的。一个格式化选项是由一个序列对构成的,每一个对包括:
- 一个下限(lower limit)
- 一个格式字符串(format string)
下限和格式字符串由一个#
符号分隔,对与对之间由符号|
分隔。
{1,choice,0#no houses|1#one house|2#{1} houses}
{1} | 结果 |
---|---|
0 | “no houses” |
1 | “one house” |
3 | “3 houses” |
-1 | “no houses” |
也可以用<,小于等于(\u2264)实现#相同的结果。
文本文件和字符集
源文件的字符编码
在程序编译和运行时,有3种字符编码参与其中:
- 源文件:本地编码
- 类文件:modified UTF-8
- 虚拟机:UTF-16
为了使源文件到处使用,则必须使用普通的ASCII吗,也就是说必须把所有非ASCII字符转换成等价的UNICODE字符。例如,spring源码中的 message资源文件。JDK自带工具,native2ascii,可以将本地字符编码转换成普通的ASCII编码。
资源包
定位资源包
当本地化一个应用时,会制造很多资源包(resource bundle)。每个包,都要为想要支持的locale提供相应的版本,格式:
包名_语言_国家
包名_语言
ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
getBundle
方法试图加载匹配当前Locale定义的语言和国家的包,如果失败,则依次通过放弃国家和语言来进行查找,然后同样的查找被应用于默认的Locale,如果最后还是不行,则去查看默认的包文件,如果失败,则抛出异常。
- 包名_当前locale的语言_当前locale的国家_当前locale的变量
- 包名_当前locale的语言_当前locale的国家
- 包名_当前locale的语言
- 包名_默认locale的语言_默认locale的国家_默认locale的变量
- 包名_默认locale的语言_默认locale的国家
- 包名_默认locale的语言
- 包名
可以使用JDK命令对属性文件中非Unicode字符进行转码
native2ascii -encoding GBK messages_zh_CN.properties m.zh
属性文件
ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
String s = resource.getString("name")
包类
为了提供字符串类型以外的资源,需要定义类,它必须扩展自ResourceBundle
类,使用标准的命名规则来命名类。
MyResource.java
Myresource_en.java
Myresource_en_UN.java
//获取资源
ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
(Color) resource.getObject("name");
(Map) resource.getObject("name");
Spring中的国际化实现
pring中国际化是通过MessageSource
这个接口来支持的
public interface MessageSource {
/**
* 获取国际化信息
* @param code 表示国际化资源中的属性名;
* @param args 用于传递格式化串占位符所用的运行参数;
* @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;
* @param locale 表示本地化对象
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
常见3个实现类
ResourceBundleMessageSource
:这个是基于Java的ResourceBundle
基础类实现,允许仅通过资源名加载国际化资源
ReloadableResourceBundleMessageSource
:这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息
StaticMessageSource
:它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。
ResourceBundleMessageSource
类继承图:
-
HierarchicalMessageSource
支持层级资源查找
public interface HierarchicalMessageSource extends MessageSource {
void setParentMessageSource(@Nullable MessageSource parent);
@Nullable
MessageSource getParentMessageSource();
}
-
MessageSourceSupport
提供 参数占位 格式化支持。
AbstractMessageSource
工厂方法模式。
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
@Nullable
private MessageSource parentMessageSource;
@Nullable
private Properties commonMessages;
private boolean useCodeAsDefaultMessage = false;
//获取消息
@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
return getDefaultMessage(code);
}
return renderDefaultMessage(defaultMessage, args, locale);
}
@Override
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes != null) {
for (String code : codes) {
String message = getMessageInternal(code, resolvable.getArguments(), locale);
if (message != null) {
return message;
}
}
}
String defaultMessage = getDefaultMessage(resolvable, locale);
if (defaultMessage != null) {
return defaultMessage;
}
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
}
/**
* 内部 实现,用于 子类扩展
*/
@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
if (code == null) {
return null;
}
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
argsToUse = resolveArguments(args, locale);
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
// Check locale-independent common messages for the given message code.
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
// Not found -> check parent, if any.
return getMessageFromParent(code, argsToUse, locale);
}
/**
* 内部 实现,用于 子类扩展
*/
@Nullable
protected abstract MessageFormat resolveCode(String code, Locale locale);
}
AbstractResourceBasedMessageSource
提供设置 basename
的能力。
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {
//保存basename。
private final Set<String> basenameSet = new LinkedHashSet<>(4);
//默认编码
@Nullable
private String defaultEncoding;
private boolean fallbackToSystemLocale = true;
//默认locale。
@Nullable
private Locale defaultLocale;
}
ResourceBundleMessageSource实现
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
//循环所有basename
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
//获取指定的MessageFormat
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
bean名称必须是
messageSource
。
Spring MVC 国际化使用
创建消息属性文件
通过basename
指定消息的名称,则对不同locale创建不同的属性文件。具体见前面内容。
示例:
ValidationMessages.properties
ValidationMessages_ar.properties
ValidationMessages_cs.properties
ValidationMessages_de.properties
ValidationMessages_en.properties
ValidationMessages_es.properties
ValidationMessages_fa.properties
ValidationMessages_fr.properties
Spring boot 的默认
basename
为 :messages
,通过spring.messages.basename
设置。属性文件配置在resources
下。同一个basename
会以Resource Bundle
目录显示。
在每个属性文件中,为每个属性设置属性值。
示例:
javax.validation.constraints.NotNull.message = must not be null
javax.validation.constraints.Null.message = must be null
javax.validation.constraints.Past.message = must be a past date
设置LocaleResolver
Spring Mvc 默认有4种 LocaleResolver。
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
AcceptHeaderLocaleResolver
接受 Accept-Language
header 来设置locale。
SessionLocaleResolver
SessionLocaleResolver
将客户端的 Locale 保存到 HttpSession
对象中,并且可以进行修改(这意味着当前环境信息,前端给浏览器发送一次即可记住,只要 session 有效,浏览器就不必再次告诉服务端当前的环境信息)
public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE";
CookieLocaleResolver
Locale会保存到cookie
中,cookieName:org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE
public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
接受的request参数,默认参数名为locale
,通过LocaleChangeInterceptor
设置,可以在构造CookieLocaleResolver
时设置。
FixedLocaleResolver
固定Locale。
spring.mvc.locale=zh_CN
#或者
spring:
web:
locale: zh_CN
locale-resolver: fixed
spring.web.locale-resolver
优先级比spring.mvc.locale-resolver
高一些。
spring.web.locale
、spring.mvc.locale
这两个配置属性,假如存在,就会成为AcceptHeaderLocaleResolver
的默认的Locale 区域对象。 并在请求响应的请求头中没有Accept-Language
这个属性时,成为AcceptHeaderLocaleResolver
返回的Locale 区域对象。
LocaleChangeInterceptor
LocaleChangeInterceptor
则主要是负责参数解析的,在配置拦截器的时候,设置了参数名为 locale
(默认),也就是说可以通过 locale
参数来传递当前的环境信息
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
//通过设置的参数,获取locale。
String newLocale = request.getParameter(getParamName());
if (newLocale != null) {
if (checkHttpMethod(request.getMethod())) {
//获取 LocaleResolver
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
//设置 locale。
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
}
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
@Bean
public LocaleResolver localeResolver() {
return new CookieLocaleResolver();
}
/**
* 切换语言按钮URL?language=zh_CN,切换后将语言信息存入cookie;
*
* @return
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
//承载 locale的参数。
lci.setParamName("language");
return lci;
}
}