Java 国际化

背景知识

通讯的发达,使得世界各地交流越来越紧密。许多的软件产品也要面向世界上不同国家的用户。其中,语言障碍显然是产品在不同语种用户中进行推广的一个重要问题。

本文围绕国际化这一主题,先介绍国际标准的语言编码,然后讲解在Java应用中如何去实现国际化。

语言编码、国家/地区编码

做 web 开发的朋友可能多多少少接触过类似 zh-cn, en-us 这样的编码字样。

这些编码是用来表示指定的国家地区的语言类型的。那么,这些含有特殊含义的编码是如何产生的呢?

ISO-639 标准使用编码定义了国际上常见的语言,每一种语言由两个小写字母表示。

ISO-3166 标准使用编码定义了国家/地区,每个国家/地区由两个大写字母表示。

下表列举了一些常见国家、地区的语言编码:

国家/地区 语言编码 国家/地区 语言编码
简体中文(中国) zh-cn 繁体中文(台湾地区) zh-tw
繁体中文(香港) zh-hk 英语(香港) en-hk
英语(美国) en-us 英语(英国) en-gb
英语(全球) en-ww 英语(加拿大) en-ca
英语(澳大利亚) en-au 英语(爱尔兰) en-ie
英语(芬兰) en-fi 芬兰语(芬兰) fi-fi
英语(丹麦) en-dk 丹麦语(丹麦) da-dk
英语(以色列) en-il 希伯来语(以色列) he-il
英语(南非) en-za 英语(印度) en-in
英语(挪威) en-no 英语(新加坡) en-sg
英语(新西兰) en-nz 英语(印度尼西亚) en-id
英语(菲律宾) en-ph 英语(泰国) en-th
英语(马来西亚) en-my 英语(阿拉伯) en-xa
韩文(韩国) ko-kr 日语(日本) ja-jp
荷兰语(荷兰) nl-nl 荷兰语(比利时) nl-be
葡萄牙语(葡萄牙) pt-pt 葡萄牙语(巴西) pt-br
法语(法国) fr-fr 法语(卢森堡) fr-lu
法语(瑞士) fr-ch 法语(比利时) fr-be
法语(加拿大) fr-ca 西班牙语(拉丁美洲) es-la
西班牙语(西班牙) es-es 西班牙语(阿根廷) es-ar
西班牙语(美国) es-us 西班牙语(墨西哥) es-mx
西班牙语(哥伦比亚) es-co 西班牙语(波多黎各) es-pr
德语(德国) de-de 德语(奥地利) de-at
德语(瑞士) de-ch 俄语(俄罗斯) ru-ru
意大利语(意大利) it-it 希腊语(希腊) el-gr
挪威语(挪威) no-no 匈牙利语(匈牙利) hu-hu
土耳其语(土耳其) tr-tr 捷克语(捷克共和国) cs-cz
斯洛文尼亚语 sl-sl 波兰语(波兰) pl-pl
瑞典语(瑞典) sv-se

注:由表中可以看出语言、国家/地区编码一般都是英文单词的缩写。

字符编码

在此处,引申一下字符编码的概念。

是不是有了语言、国家/地区编码,计算机就可以识别各种语言了?

答案是否。作为程序员,相信每个人都会遇到过这样的情况:期望打印中文,结果输出的却是乱码。

这种情况,往往是因为字符编码的问题。

计算机在设计之初,并没有考虑多个国家,多种不同语言的应用场景。当时定义一种ASCII码,将字母、数字和其他符号编号用7比特的二进制数来表示。后来,计算机在世界开始普及,为了适应多种文字,出现了多种编码格式,例如中文汉字一般使用的编码格式为GB2312GBK

由此,又产生了一个问题,不同字符编码之间互相无法识别。于是,为了一统江湖,出现了 unicode 编码。它为每种语言的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台的文本转换需求。

有人不禁要问,既然 Unicode 可以支持所有语言的字符,那还要其他字符编码做什么?

Unicode 有一个缺点:为了支持所有语言的字符,所以它需要用更多位数去表示,比如 ASCII 表示一个英文字符只需要一个字节,而 Unicode 则需要两个字节。很明显,如果字符数多,这样的效率会很低。

为了解决这个问题,有出现了一些中间格式的字符编码:如 UTF-8UTF-16UTF-32 等(中国的程序员一般使用UTF-8编码)。

Java 中实现国际化

国际化的实现原理很简单:

  1. 先定义好不同语种的模板;
  2. 选择语种;
  3. 加载指定语种的模板。

接下来,本文会按照步骤逐一讲解实现国际化的具体步骤

定义不同语种的模板

Java中将多语言文本存储在格式为 properties 的资源文件中。

它必须遵照以下的命名规范:

<资源名>_<语言代码>_<国家/地区编码>.properties

其中,语言编码和国家/地区编码都是可选的。

注:<资源名>.properties 命名的国际化资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。

定义properties文件

src/main/resources/locales 路径下定义名为content的不同语种资源文件:

content_en_US.properties

helloWorld = HelloWorld!
time = The current time is %s.

content_zh_CN.properties

helloWorld = \u4e16\u754c\uff0c\u4f60\u597d\uff01
time = \u5f53\u524d\u65f6\u95f4\u662f\u0025\u0073\u3002

可以看到:几个资源文件中,定义的Key 完全一致,只是 Value 是对应语言的字符串。

虽然属性值各不相同,但属性名却是相同的,这样应用程序就可以通过Locale对象和属性名精确调用到某个具体的属性值了。

Unicode 转换工具

上一节中,我们定义的中文资源文件中的属性值都是以\u开头的四位16进制数。其实,这表示的是一个 Unicode 编码。

helloWorld = \u4e16\u754c\uff0c\u4f60\u597d\uff01
time = \u5f53\u524d\u65f6\u95f4\u662f\u0025\u0073\u3002

本文的字符编码中提到了,为了达到跨编码也正常显示的目的,有必要将非 ASCII 字符转为 Unicode 编码。上面的中文资源文件就是中文转为 Unicode 的结果。

怎么将非 ASCII 字符转为 Unicode 编码呢?

JDK在bin目录下为我们提供了一个转换工具:native2ascii

它可以将中文字符的资源文件转换为 Unicode 代码格式的文件,命令格式如下:

native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]

假设content_zh_CN.propertiesd:\ 目录。执行以下命令可以新建一个名为 content_zh_CN_new.properties 的文件,其中的内容就中文字符转为 UTF-8 编码格式的结果。

native2ascii -encoding utf-8 d:\content_zh_CN.properties d:\content_zh_CN_new.properties

选择语种

定义了多语言资源文件,第二步就是根据本地语种选择模板文件了。

Locale

在 Java 中,一个 java.util.Locale 对象表示了特定的地理、政治和文化地区。需要 Locale 来执行其任务的操作称为语言环境敏感的操作,它使用 Locale 为用户量身定制本地信息。

它有三个构造方法

Locale(String language) :根据语言编码初始化
Locale(String language, String country) :根据语言编码、国家编码初始化
Locale(String language, String country, String variant) :根据语言编码、国家编码、变体初始化

此外,Locale 定义了一些常用的Locale常量:Locale.ENGLISHLocale.CHINESE 等。

// 初始化一个通用英语的locale.
Locale locale1 = new Locale("en");
// 初始化一个加拿大英语的locale.
Locale locale2 = new Locale("en", "CA");
// 初始化一个美式英语变种硅谷英语的locale
Locale locale3 = new Locale("en", "US", "SiliconValley");
// 根据Locale常量初始化一个简体中文
Locale locale4 = Locale.SIMPLIFIED_CHINESE;

加载指定语种的模板

ResourceBoundle

Java为我们提供了用于加载本地化资源文件的工具类:java.util.ResourceBoundle

ResourceBoundle 提供了多个名为 getBundle 的静态重载方法,这些方法的作用是用来根据资源名、Locale选择指定语种的资源文件。需要说明的是: getBundle 方法的第一个参数一般都是baseName ,这个参数表示资源文件名。

ResourceBoundle 还提供了名为 getString 的方法,用来获取资源文件中key对应的value。

public static void main(String[] args) {
    // 根据语言+地区编码初始化
    ResourceBundle rbUS = ResourceBundle.getBundle("locales.content", new Locale("en", "US"));
    // 根据Locale常量初始化
    ResourceBundle rbZhCN = ResourceBundle.getBundle("locales.content", Locale.SIMPLIFIED_CHINESE);
    // 获取本地系统默认的Locale初始化
    ResourceBundle rbDefault = ResourceBundle.getBundle("locales.content");
    // ResourceBundle rbDefault =ResourceBundle.getBundle("locales.content", Locale.getDefault()); // 与上行代码等价

    System.out.println("us-US:" + rbUS.getString("helloWorld"));
    System.out.println("us-US:" + String.format(rbUS.getString("time"), "08:00"));
    System.out.println("zh-CN:" + rbZhCN.getString("helloWorld"));
    System.out.println("zh-CN:" + String.format(rbZhCN.getString("time"), "08:00"));
    System.out.println("default:" + rbDefault.getString("helloWorld"));
    System.out.println("default:" + String.format(rbDefault.getString("time"), "08:00"));
}

输出

us-US:HelloWorld!
us-US:The current time is 08:00.
zh-CN:世界,你好!
zh-CN:当前时间是08:00。
default:世界,你好!
default:当前时间是08:00。

注:在加载资源时,如果指定的本地化资源文件不存在,它会尝试按下面的顺序加载其他的资源:本地系统默认本地化对象对应的资源 -> 默认的资源。如果指定错误,Java会提示找不到资源文件。

支持国际化的国际化工具类

Java 中也提供了几个支持国际化的格式化工具类。例如:NumberFormatDateFormatMessageFormat

NumberFormat

NumberFormat 是所有数字格式类的基类。它提供格式化和解析数字的接口。它也提供了决定数字所属语言类型的方法。

public static void main(String[] args) {
    double num = 123456.78;
    NumberFormat format = NumberFormat.getCurrencyInstance(Locale.SIMPLIFIED_CHINESE);
    System.out.format("%f 的本地化(%s)结果: %s", num, Locale.SIMPLIFIED_CHINESE, format.format(num));
}

DateFormat

DateFormat 是日期、时间格式化类的抽象类。它支持基于语言习惯的日期、时间格式。

public static void main(String[] args) {
    Date date = new Date();
    DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ENGLISH);
    DateFormat df2 = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.SIMPLIFIED_CHINESE);
    System.out.format("%s 的本地化(%s)结果: %s\n", date, Locale.SIMPLIFIED_CHINESE, df.format(date));
    System.out.format("%s 的本地化(%s)结果: %s\n", date, Locale.SIMPLIFIED_CHINESE, df2.format(date));
}

MessageFormat

Messageformat 提供一种与语言无关的拼接消息的方式。通过这种拼接方式,将最终呈现返回给使用者。

public static void main(String[] args) {
    String pattern1 = "{0},你好!你于  {1} 消费  {2} 元。";
    String pattern2 = "At {1,time,short} On {1,date,long},{0} paid {2,number, currency}.";
    Object[] params = {"Jack", new GregorianCalendar().getTime(), 8888};
    String msg1 = MessageFormat.format(pattern1, params);
    MessageFormat mf = new MessageFormat(pattern2, Locale.US);
    String msg2 = mf.format(params);
    System.out.println(msg1);
    System.out.println(msg2);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容