欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网站国际化实现(1)—JDK的国际化支持

程序员文章站 2022-04-16 11:05:42
...

一、背景

很多网站的用户分布在世界各地,因此网站需要针对不同国家的用户展示不同语言的内容,因此就有了国际化实现的需求,大多数网站都会在网站的头部或尾部设置语言切换链接,这样就可以直接切换成相应的内容。其中有些网站是通过网站地址或参数进行区分,有些是通过设置cookie值进行进行区分。

这里先不讲网站具体的实现,先介绍下网站国际化需要的基础知识,即JDK本身对国际化的支持。这里说明下JDK本身的国际化只是网站国际化实现的基础,其本身还可以支持GUI程序或其它应用程序的国际化实现。

二、简介

JAVA官方国际化教程

国际化(Internationalization )用于便捷地支持不同语言或区域的处理,国际化有时简称为 i18n,取Internationalization单词的首字母和尾字母,中间因为还有18个字母,用18代替,故简写为i18n。

一般需要国际化处理的数据有时间、数字、金额、文本等。国际化一般有本地化的数据,而且通常都不是硬编码的,不需要每次修改都重新编译,而且还需要处理非常便捷。

国际化的整个过程可以大致分为三步:本地化、数据获取、格式化。下面再详细说明下。

三、本地化

既然要做到国际化,那么首先肯定得知道是哪个语言或区域,这个如何去获取或设置呢?JDK提供了Locale类去抽象本地化实现,Locale对象表示了特定的地理、政治和文化地区。

Locale有几个重要的编码这里先介绍下:

  1. 语言编码(Language Code): 两到三位符合ISO 639 标准的字母。这个编码比较好理解,主要用作不同语言的定义。语言编码参照表链接
  2. 脚本编码(Script Code):由一个大写首字母+三个小写字母组成,符合ISO 15924标准的编码。这个编码JDK7以后才引入,主要用于区分同一语言同一国家地区使用不同的书写系统的情形,例如uz-Cyrl-UZ表示使用西里尔字母的乌兹别克语。脚本编码参照表链接
  3. 区域编码(Region Code):由两个或者三个大写符合ISO 3166标准的字母组成。 这个编码主要用于表示国家或者地区。区域编码参照表链接
  4. 多样编码(Variant Code):这个编码在JDK7以前常用于定义语言或者区域之外的区别,比如计算平台Windows或UNIX。但是IETF BCP 47标准不建议这么使用。所以JDK7之后,多样编码(Variant Code)主要用来定义一门语言后者方言的多样性。多样编码参照表链接
    而前面说到的非语言的多样性,比如平台的区别(Windows, UNIX, Linux)或者发布信息(6u23 or JDK 7)等,JDK7引入Unicode Locale Extensions支持来符合IETF BCP 47标准。

JDK8支持的本地化一览链接(Supported Locales栏)

当然,在实际Locale使用中可能用不到所有的编码定义或拓展,大多数情况下语言编码和区域编码就足够区分定义,不过了解这些编码的含义与作用对使用上还是有好处的。实际上Locale对象的创建就是根据上述的编码和拓展定义出来的。

这里以JDK8为例,Locale的创建可以通过Locale.Builder类、Locale本身的构造方法、forLanguageTag方法、或者预先定义好的常量进行创建。当然getDefault方法也可以得到基于当前环境默认的Locale对象。这里方法上各有差异,本质还是设置前面说到的编码或拓展值。

四、数据获取

得到了本地化信息,那么下一步就是要获取对应的数据。前面提到过国际化需要信息不是硬编码的,这样就不要每次修改都重新编译,而且也易于维护。

在JDK中,数据隔离和获取一般使用ResourceBundle类配合properties文件使用,实际使用中,一般会定义一些properties文件,文件名前缀相同,后缀跟一些本地化的信息,这样不同的文件就可以存储不同本地化对应的数据。

这里说得太抽象,直接上结合官网示例修改的代码,为了便于阅读,下面列个大概,具体请看我上传的github项目代码

 

public class ResourceBundleDemo {

public static void main(String[] args) {
    // 这里用到的i18n下面的文件名都以下划线分隔,RBControl_语言编码_区域编码的形式
    String baseName = "i18n/RBControl";

    // 演示Locale常量解析RBControl_zh_cn.properties数据
    Locale l = Locale.CHINA;
    ResourceBundle rs = ResourceBundle.getBundle(baseName, l);
    String result = rs.getString("region");
    System.out.println("示例1结果:" + result);

    // 演示Locale.Builder解析RBControl_zh_hk.properties数据
    l = new Locale.Builder().setLanguage("zh").setRegion("hk").build();
    rs = ResourceBundle.getBundle(baseName, l);
    result = rs.getString("region");
    System.out.println("示例2结果:" + result);

    // 演示Locale构造函数解析RBControl_zh_tw.properties数据
    l = new Locale("zh", "tw");
    rs = ResourceBundle.getBundle(baseName, l);
    result = rs.getString("region");
    System.out.println("示例3结果:" + result);

    // 演示Locale构造函数解析RBControl_en_US.properties数据
    l = Locale.forLanguageTag("en-US");
    rs = ResourceBundle.getBundle(baseName, l);
    result = rs.getString("region");
    System.out.println("示例4结果:" + result);

    // 演示Locale解析RBControl_zh.properties数据,但是对应数据不存在时,会取默认RBControl.properties
    l = new Locale("zh");
    rs = ResourceBundle.getBundle(baseName, l);
    result = rs.getString("region");
    System.out.println("示例5结果:" + result);
}
}

 

网站国际化实现(1)—JDK的国际化支持
            
    
    博客分类: 行业应用 国际化JDK实践
Paste_Image.png

对于ResourceBundle,在指定的locale找不到的时候,getBundle方法会找最相近的
值。例如官网中举例ButtonLabel_fr_CA_UNIX是文件名,Locale默认是en_US,getBundle方法会按照如下的顺序查找ButtonLabel_fr_CA_UNIX、ButtonLabel_fr_CA、ButtonLabel_fr、ButtonLabel_en_US、ButtonLabel_en、ButtonLabel,如果getBundle在列表中找不到匹配,会抛出MissingResourceException异常,所以为了避免这个异常,最好每次都使用没有后缀的文件,在前面示例中就是ButtonLabel文件名。

五、格式化

上次已经可以获取到数据了,有些时候数据获取到之后可以直接展示,但是如果涉及到时间、数字、金额、动态文本等数据时,又需要额外做下处理了,因为本身这些数据就是本地化敏感的,那么这个时候怎么办呢?这时就需要对相应的数据进行格式化操作。下面详细做下说明。

5.1 数字与金额

数字与金额其实都是数值相关的处理,JDK提供了NumberFormat类进行处理,处理过程可以大致分为两步:(1)getInstance方法得到实例;(2)format方法格式化数据。

比如long、long可以使用NumberFormat.getNumberInstance(Locale inLocale)方法获得相应本地化的对象实例,比如int可以使用getIntegerInstance(Locale inLocale)方法获得对应实例,金额可以调用getCurrencyInstance(Locale inLocale)方法得到实例,还有百分比的情况可以调用getPercentInstance(Locale inLocale)得到实例;最后再调用format方法即可。

这里额外还说下DecimalFormat类,这个类主要做小数的格式化处理。比如有不少场景对于123456.789这样的数字要格式化成123,456.789 ;这个时候DecimalFormat就非常实用。简单示例如下:
NumberFormat nf = NumberFormat.getNumberInstance(locale);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern("###,###.###");
String output = df.format(value);

上面可以看到DecimalFormat格式化时会需要有个格式化的模式"###,###.###",而这个模式还可以支持更多灵活的语法。基本如下:

符号 含义
0 阿拉伯数字
# 阿拉伯数字,0如果无效的话就不显示
. 小数的分隔符
, 分组的分隔符
E 分隔科学计数法中的尾数和指数
; 格式化分隔符,分隔正数和负数子模式
- 默认的负数前缀
% 乘以100,百分数展示
? 乘以1000,千分数展示
¤ 货币记号,由货币符号替换。如果两个同时出现,则用国际货币符号替换。如果出现在某个模式中,则使用货币小数分隔符,而不使用小数分隔符
X 任意可以用在前缀或后缀的字符
' 用于在前缀或或后缀中为特殊字符加引号,例如 "'#'#" 将 123 格式化为 "#123";如果要创建单引号本身,就使用两个单引号"# 9''123"

这里有两个不太常用到的点做下说明:(1)格式里面有分号作分隔符,其实完整的模式应该是subpattern;subpattern,前一个subpattern是正数的格式化模式,后一个subpattern是负数的格式化模式,每一个subpattern的形式都可以用前面表格的去定义表示,不过负数的格式化模式是可选的,通常情况下不会用;(2)前面表格的分隔符还可以定制化,使用DecimalFormatSymbols类就可以自定义分隔符,具体使用时调用含DecimalFormatSymbols参数的DecimalFormat构造方法,再进行格式化处理即可。

5.2 日期与时间

日期与时间的处理,以前主要用到SimpleDateFormat这个实现类,JDK8新引进了java.time包下的DateTimeFormatter类也可以进行格式化处理。DateTimeFormatter可以看我前面写的JDK8新特性一览里面的介绍,下面以SimpleDateFormat举例,:
SimpleDateFormat formatter = new SimpleDateFormat(pattern, currentLocale);
Date today = new Date();
String output = formatter.format(today);
System.out.println(pattern + " " + output);

这里同样有个格式化语法

符号 含义 类型 示例
G 纪元 Text AD
y 年份 Number 2009
M 月(在一年中的月分) Text & Number July & 07
d 日(在一个月中的天数) Number 10
h 小时(12小时制,1-12) Number 12
H 小时(24小时制,0-23) Number 0
m Number 30
s Number 55
S 毫秒 Number 978
E 日(在一周中的天数) Text Tuesday
D 日(在一年中的天数) Number 189
F 第几周(这一天在这一个月的第几周) Number 2 (2nd Wed in July)
w 第几周(在一年的第几周) Number 27
W 第几周(这个月的第几周) Number 2
a 上午/下午(am/pm) Text PM
k 小时(24小时制,1-24) Number 24
K 小时(12小时制,0-11) Number 0
z 时区 Text Pacific Standard Time
' 文本分隔(格式化内容中插入文本时用到) Delimiter (none)
' 单引号 Literal '

5.3 文本

在网站应用里面,文本国际化应该是最常用到的了。而且复杂情况下,文本可能还是是固定不变的,可能是动态数据,还可能包含前面讲的金额或时间等信息。比如文本是“我在xxx时间,在xxx网站,花费了xxx钱,购买了xxx东西”,这个时候时间、站名、金额、东西都不一样。不过JDK的MessageFormat类提供了简便的实现。

主要的步骤可以分为三步:(1)定义文本模板;(2)初始化MessageFormat类;(3)根据模板和动态参数进行格式化处理。下面是简单示例:

网站国际化实现(1)—JDK的国际化支持
            
    
    博客分类: 行业应用 国际化JDK实践
定义模板.png

 

ResourceBundle messages =  ResourceBundle.getBundle("i18n/Message",currentLocale);
Object[] messageArguments = {new Date(), messages.getString("goods"),"taobao",65.00};
MessageFormat formatter = new MessageFormat(messages.getString("template"),currentLocale );  
String output = formatter.format(messageArguments);  
System.out.println(output);

详细代码示例可以看我上传的github项目代码

通过上面的示例可以看到,MessageFormat类会自动将传为的参数,按照ResourceBundle类获取的模板要求做相应的格式化处理,这样就可以满足动态数据的展示了。上面在定义文本模板时用到了类似{3,number,currency}这样的写法,表示第三个参数格式类型为数字,形式用金额形式。这里也可以用{3}或者{3,nmuber}这样就会相应的默认形式格式化。具体语法详细讲解链接

另外在有些语言环境下,复数的表现形式不同,比如英语环境下,one file、two files,这个时候的模板直接定义成{0}file这种形式就不太合适,这个时候就可以用到ChoiceFormat类进行处理。

通过上面的三个步骤(本地化—数据获取—格式化),整个国际化的过程就完成了。当然简单情况下本地化—数据获取两步也可能

最后还啰嗦一句,由于上面的每个点展开讲都可以写一篇甚至几篇博文,限于篇幅,笔者主要把概念和常用部分重点做了强调,有了清晰的概念介绍与示例,对于大家的理解应该还是很有帮助的。不过这里还是强烈建议大家仔细阅读下JAVA官方国际化教程,里面讲解得非常详细,而且有更多示例,笔者的一些示例也是在官方示例上面做的修改。