美文网首页程序员
网站国际化实现(1)—JDK的国际化支持

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

作者: 丘八老爷 | 来源:发表于2017-05-20 15:24 被阅读333次

    一、背景

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

    <pre><code>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);
    }
    

    }</pre></code>


    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就非常实用。简单示例如下:
    <code>
    NumberFormat nf = NumberFormat.getNumberInstance(locale);
    DecimalFormat df = (DecimalFormat)nf;
    df.applyPattern("###,###.###");
    String output = df.format(value);
    </code>

    上面可以看到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举例,:
    <code>
    SimpleDateFormat formatter = new SimpleDateFormat(pattern, currentLocale);
    Date today = new Date();
    String output = formatter.format(today);
    System.out.println(pattern + " " + output);</code>

    这里同样有个格式化语法

    符号 含义 类型 示例
    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)根据模板和动态参数进行格式化处理。下面是简单示例:


    定义模板.png

    <pre><code>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);</pre></code>

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

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

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

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

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

    相关文章

      网友评论

        本文标题:网站国际化实现(1)—JDK的国际化支持

        本文链接:https://www.haomeiwen.com/subject/qklcxxtx.html