4

Java 国际化

 11 months ago
source link: https://dunwu.github.io/01.Java/01.JavaSE/02.%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7/03.Java%E5%9B%BD%E9%99%85%E5%8C%96.html#messageformat
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

# 背景知识

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

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

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

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

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

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

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

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

国家/地区语言编码国家/地区语言编码
简体中文(中国)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。

import java.util.Locale;
import java.util.ResourceBundle;

public class ResourceBundleDemo {

    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("en-US:" + rbUS.getString("helloWorld"));
        System.out.println("en-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"));
    }

}

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

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

# 支持国际化的工具类

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

# NumberFormat

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

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatDemo {

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

}

// 输出:
// 123456.780000 的国际化(zh_CN)结果: ¥123,456.78

# DateFormat

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

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormatDemo {

    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.ENGLISH, df.format(date));
        System.out.format("%s 的国际化(%s)结果: %s\n", date, Locale.SIMPLIFIED_CHINESE, df2.format(date));
    }

}

// 输出
// Fri Dec 23 11:14:45 CST 2022 的国际化(en)结果: Dec 23, 2022
// Fri Dec 23 11:14:45 CST 2022 的国际化(zh_CN)结果: 2022-12-23

# MessageFormat

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

import java.text.MessageFormat;
import java.util.GregorianCalendar;
import java.util.Locale;

public class MessageFormatDemo {

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

}

// 输出:
// Jack,你好!你于 22-12-23 上午11:05 消费 8,888 元。
// At 11:05 AM On December 23, 2022,Jack paid $8,888.00.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK